From cf6f65c5eab5d7d9d0f86fb8b6b3841f7b0f535a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Mar 2026 16:56:40 +0100 Subject: [PATCH 1/8] stat-util: also include inode type in hash ops This doesn't really have any major benefit, but it does make this nicely mirror stat_inode_same() which also checks this triplet for identifying identical inodes. --- src/basic/stat-util.c | 10 +++++++++- src/basic/stat-util.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index ef39562992..98dfa8c5a9 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -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,7 +722,11 @@ 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); diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index c261014cd2..939a0fc398 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -122,6 +122,7 @@ 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; From 172ae47e659c76c66a2e00ff89f277cf9ce2b5f4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 24 Mar 2026 17:06:09 +0100 Subject: [PATCH 2/8] stat-util: introduce inode_unmodified_hash_ops This is almost the same as inode_hash_ops, but also hashes + compares all attributes that could affect the contents of a file. It ignores "superficial"/"external" attributes such as ownership or access mode however. --- src/basic/stat-util.c | 53 +++++++++++++++++++++++++++++++++++++++++++ src/basic/stat-util.h | 6 +++++ 2 files changed, 59 insertions(+) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 98dfa8c5a9..ac218d1552 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -731,6 +731,59 @@ int inode_compare_func(const struct stat *a, const struct stat *b) { 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 diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index 939a0fc398..2b50c9c558 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -127,6 +127,12 @@ 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 From 3839f5efec3abc24af880fe77aa5e8736116324d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 20 Mar 2026 14:59:27 +0100 Subject: [PATCH 3/8] json-util: optionally accept string-based serialization for IPv4 addresses --- src/libsystemd/sd-json/json-util.c | 16 ++++- src/test/test-json.c | 103 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index 7f90b7fc79..32578168db 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -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,25 @@ 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; diff --git a/src/test/test-json.c b/src/test/test-json.c index 4994d42abc..e9650339a1 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include +#include #include #include #include @@ -1630,4 +1631,106 @@ 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); +} + DEFINE_TEST_MAIN(LOG_DEBUG); From 9eb207356c9c75e80e976ee4cb1ad8a11ce8972f Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 20 Mar 2026 15:16:19 +0100 Subject: [PATCH 4/8] json-util: add json_dispatch_in6_addr() --- src/libsystemd/sd-json/json-util.c | 34 +++++++++++ src/libsystemd/sd-json/json-util.h | 1 + src/test/test-json.c | 94 ++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+) diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c index 32578168db..c321579ef5 100644 --- a/src/libsystemd/sd-json/json-util.c +++ b/src/libsystemd/sd-json/json-util.c @@ -214,6 +214,40 @@ int json_dispatch_in_addr(const char *name, sd_json_variant *variant, sd_json_di 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; +} + int json_dispatch_const_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { const char **p = ASSERT_PTR(userdata), *path; diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h index 847725a41e..478d2a2a21 100644 --- a/src/libsystemd/sd-json/json-util.h +++ b/src/libsystemd/sd-json/json-util.h @@ -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); diff --git a/src/test/test-json.c b/src/test/test-json.c index e9650339a1..d6308b23e7 100644 --- a/src/test/test-json.c +++ b/src/test/test-json.c @@ -1733,4 +1733,98 @@ TEST(json_dispatch_in_addr) { &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); From 333a9a91ef47f9f9bd226015df819a8aafed7a71 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Feb 2026 15:51:09 +0100 Subject: [PATCH 5/8] dns-rr: tighten rules on parsing RR keys from JSON let's ensure the name is actually a valid DNS name. --- src/shared/dns-rr.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/dns-rr.c b/src/shared/dns-rr.c index 0fa730c13b..58d26e3609 100644 --- a/src/shared/dns-rr.c +++ b/src/shared/dns-rr.c @@ -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; From 2f1bbebeb96e6745bb13df70d8dbcc99d79b9cdd Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Feb 2026 15:50:26 +0100 Subject: [PATCH 6/8] dns-rr: add dns_resource_record_from_json() This only parses a small subset of RR types for now, but we can add more later. Covered are the most important RR types: A, AAAA, PTR. --- src/resolve/test-dns-rr.c | 43 +++++++++++++++++- src/shared/dns-rr.c | 95 ++++++++++++++++++++++++++++++++++++++- src/shared/dns-rr.h | 1 + 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/resolve/test-dns-rr.c b/src/resolve/test-dns-rr.c index e45f1d3423..2ded6b0ab9 100644 --- a/src/resolve/test-dns-rr.c +++ b/src/resolve/test-dns-rr.c @@ -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); diff --git a/src/shared/dns-rr.c b/src/shared/dns-rr.c index 58d26e3609..e807cef363 100644 --- a/src/shared/dns-rr.c +++ b/src/shared/dns-rr.c @@ -2308,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) @@ -2514,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", diff --git a/src/shared/dns-rr.h b/src/shared/dns-rr.h index c30cd71cfa..d747083aa8 100644 --- a/src/shared/dns-rr.h +++ b/src/shared/dns-rr.h @@ -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); From 7ad4411a6105fac649bacefc783d597b317135da Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 4 Mar 2026 17:08:03 +0100 Subject: [PATCH 7/8] resolved: also flush /etc/hosts on reload When we are told to reload our configuration also flush out /etc/hosts explicitly. This is particularly relevant since we suppress too frequent reloads, and hence a synchronous way to force a reload is very useful. --- src/resolve/resolved-etc-hosts.c | 1 + src/resolve/resolved-manager.c | 1 + 2 files changed, 2 insertions(+) diff --git a/src/resolve/resolved-etc-hosts.c b/src/resolve/resolved-etc-hosts.c index e9100de522..00c76a9977 100644 --- a/src/resolve/resolved-etc-hosts.c +++ b/src/resolve/resolved-etc-hosts.c @@ -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) { diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index a0fb74ec35..19ff92bfca 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -659,6 +659,7 @@ 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_set_defaults(m); From 0718a21c13ef6402fd153835781d13b2f916653d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 26 Feb 2026 15:54:14 +0100 Subject: [PATCH 8/8] resolved: add ability to define additional local RRs via drop-ins This is an extension of the /etc/hosts concept, but can provide any kind of RRs (well, actually, we only parse A/AAAA/PTR for now, but the concept is open for more). Fixes: #17791 --- man/resolved.conf.xml | 24 ++- man/rules/meson.build | 1 + man/systemd-resolved.service.xml | 11 ++ man/systemd.rr.xml | 95 +++++++++++ src/resolve/meson.build | 1 + src/resolve/resolved-dns-query.c | 36 ++++ src/resolve/resolved-gperf.gperf | 1 + src/resolve/resolved-manager.c | 5 + src/resolve/resolved-manager.h | 6 + src/resolve/resolved-static-records.c | 226 ++++++++++++++++++++++++++ src/resolve/resolved-static-records.h | 7 + src/resolve/resolved.conf.in | 1 + test/units/TEST-75-RESOLVED.sh | 49 ++++++ 13 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 man/systemd.rr.xml create mode 100644 src/resolve/resolved-static-records.c create mode 100644 src/resolve/resolved-static-records.h diff --git a/man/resolved.conf.xml b/man/resolved.conf.xml index 9adc0143c7..f8899fe662 100644 --- a/man/resolved.conf.xml +++ b/man/resolved.conf.xml @@ -363,12 +363,33 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953 ReadEtcHosts= Takes a boolean argument. If yes (the default), systemd-resolved will read /etc/hosts, and try to resolve - hosts or address by using the entries in the file before sending query to DNS servers. + hosts or addresses by using the entries in the file before sending query to DNS servers. + + ReadStaticRecords= + Takes a boolean argument. If yes (the default), + systemd-resolved will read + /etc/systemd/resolve/static.d/*.rr, + /run/systemd/resolve/static.d/*.rr, + /usr/local/lib/systemd/resolve/static.d/*.rr, + /usr/lib/systemd/resolve/static.d/*.rr, and try to resolve lookups by using the + entries in these files before sending query to DNS servers. This functionality is very similar to the + one controlled by ReadEtcHosts=, but allows more flexible control of DNS resource + records fields beyond just A/AAAA/PTR. See + systemd.rr5 for + details. + + If both this option and ReadEtcHosts= are enabled then this mechanism takes + precedence: any records discovered via static resource records will take precedence over records + under the same name from /etc/hosts. + + + + ResolveUnicastSingleLabel= Takes a boolean argument. When false (the default), @@ -418,6 +439,7 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953 systemd-resolved.service8 systemd-networkd.service8 dnssec-trust-anchors.d5 + systemd.rr5 resolv.conf5 diff --git a/man/rules/meson.build b/man/rules/meson.build index d2d26abe5d..682f55c774 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1280,6 +1280,7 @@ manpages = [ ['systemd.pcrlock', '5', ['systemd.pcrlock.d'], ''], ['systemd.preset', '5', [], ''], ['systemd.resource-control', '5', [], ''], + ['systemd.rr', '5', [], 'ENABLE_RESOLVE'], ['systemd.scope', '5', [], ''], ['systemd.service', '5', [], ''], ['systemd.slice', '5', [], ''], diff --git a/man/systemd-resolved.service.xml b/man/systemd-resolved.service.xml index a5ab48d2fa..1d27d2c3c5 100644 --- a/man/systemd-resolved.service.xml +++ b/man/systemd-resolved.service.xml @@ -131,6 +131,16 @@ The hostname _localdnsproxy is resolved to the IP address 127.0.0.54, i.e. the address the local DNS proxy (see above) is listening on. + The files matching /etc/systemd/resolve/static.d/*.rr, + /run/systemd/resolve/static.d/*.rr, + /usr/local/lib/systemd/resolve/static.d/*.rr, + /usr/lib/systemd/resolve/static.d/*.rr may be used to define arbitrary + records. See + systemd.rr5 for + details. Support for this may be disabled with ReadStaticRecords=no, see + resolved.conf5. + + The mappings defined in /etc/hosts are resolved to their configured addresses and back, but they will not affect lookups for non-address types (like MX). Support for /etc/hosts may be disabled with ReadEtcHosts=no, @@ -510,6 +520,7 @@ search foobar.com barbar.com systemd1 resolved.conf5 systemd.dns-delegate5 + systemd.rr5 systemd.dnssd5 dnssec-trust-anchors.d5 nss-resolve8 diff --git a/man/systemd.rr.xml b/man/systemd.rr.xml new file mode 100644 index 0000000000..d6718ccf48 --- /dev/null +++ b/man/systemd.rr.xml @@ -0,0 +1,95 @@ + + + + + + + + systemd.rr + systemd + + + + systemd.rr + 5 + + + + systemd.rr + Local static DNS resource record definitions + + + + + /etc/systemd/resolve/static.d/*.rr + /run/systemd/resolve/static.d/*.rr + /usr/local/lib/systemd/resolve/static.d/*.rr + /usr/lib/systemd/resolve/static.d/*.rr + + + + + Description + + *.rr files may be used to define resource record sets ("RRsets") that shall be + resolvable locally, similar in style to address records defined by /etc/hosts (see + hosts5 for + details). These files are read by + systemd-resolved.service8, + and are used to synthesize local responses to local queries matching the defined resource record set. + + These drop-in files are in JSON format. Each file may either contain a single top-level DNS RR + object, or an array of one or more DNS RR objects. Each RR object has at least a key + subobject consisting of a name string field and a type integer + field (which contains the RR type in numeric form). Depending on the chosen type the RR object also has + the following fields: + + + For A/AAAA RRs, the RR object should have an address field set to + either an IP address formatted as string, or an array consisting of 4 or 16 8-bit unsigned integers for + the IP address. + + For PTR/NS/CNAME/DNAME RRs, the RR object should have a name field + set to the name the record shall point to. + + + This JSON serialization of DNS RRs matches the one returned by resolvectl. + + Currently no other RR types are supported. + + + + Examples + + Simple A Record + To make local address lookups for foobar.example.com resolve to the + 192.168.100.1 IPv4 address, create + /run/systemd/resolve/static.d/foobar_example_com.rr: + + +{ + "key" : { + "type" : 1, + "name" : "foobar.example.com" + }, + "address" : [ 192, 168, 100, 1 ] +} + + + + + + See Also + + systemd1 + systemd-resolved.service8 + resolved.conf5 + hosts5 + resolvectl1 + + + + diff --git a/src/resolve/meson.build b/src/resolve/meson.build index be2979343f..b9b2e24b18 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -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', ) diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c index a0ef750447..6ec6569ae7 100644 --- a/src/resolve/resolved-dns-query.c +++ b/src/resolve/resolved-dns-query.c @@ -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; diff --git a/src/resolve/resolved-gperf.gperf b/src/resolve/resolved-gperf.gperf index c548320449..8b8a66d036 100644 --- a/src/resolve/resolved-gperf.gperf +++ b/src/resolve/resolved-gperf.gperf @@ -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) diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index 19ff92bfca..25a51ed02b 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -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; @@ -660,6 +662,7 @@ static int manager_dispatch_reload_signal(sd_event_source *s, const struct signa 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); @@ -730,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, @@ -918,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); diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index 4f595e6d04..d72e9104d7 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -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; diff --git a/src/resolve/resolved-static-records.c b/src/resolve/resolved-static-records.c new file mode 100644 index 0000000000..4aa6f2e421 --- /dev/null +++ b/src/resolve/resolved-static-records.c @@ -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; +} diff --git a/src/resolve/resolved-static-records.h b/src/resolve/resolved-static-records.h new file mode 100644 index 0000000000..f50c70ef45 --- /dev/null +++ b/src/resolve/resolved-static-records.h @@ -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); diff --git a/src/resolve/resolved.conf.in b/src/resolve/resolved.conf.in index 656bc7c0eb..147d30845b 100644 --- a/src/resolve/resolved.conf.in +++ b/src/resolve/resolved.conf.in @@ -39,6 +39,7 @@ #DNSStubListener=yes #DNSStubListenerExtra= #ReadEtcHosts=yes +#ReadStaticRecords=yes #ResolveUnicastSingleLabel=no #StaleRetentionSec=0 #RefuseRecordTypes= diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh index b3656da940..bb1cf9576c 100755 --- a/test/units/TEST-75-RESOLVED.sh +++ b/test/units/TEST-75-RESOLVED.sh @@ -1487,6 +1487,55 @@ EOF grep -qF "1.2.3.4" "$RUN_OUT" } +testcase_static_record() { + mkdir -p /run/systemd/resolve/static.d/ + cat >/run/systemd/resolve/static.d/statictest.rr </run/systemd/resolve/static.d/statictest2.rr </run/systemd/resolve/static.d/garbage.rr </run/systemd/resolve/static.d/garbage2.rr <