diff --git a/accounts-service/com.endlessm.ParentalControls.Dns.xml b/accounts-service/com.endlessm.ParentalControls.Dns.xml
new file mode 100644
index 0000000..fe5967c
--- /dev/null
+++ b/accounts-service/com.endlessm.ParentalControls.Dns.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/accounts-service/com.endlessm.ParentalControls.policy.in b/accounts-service/com.endlessm.ParentalControls.policy.in
index ceae227..295f301 100644
--- a/accounts-service/com.endlessm.ParentalControls.policy.in
+++ b/accounts-service/com.endlessm.ParentalControls.policy.in
@@ -40,6 +40,46 @@
+
+ Change your own DNS servers
+ Authentication is required to change your DNS servers.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Read your own DNS servers
+ Authentication is required to read your DNS servers.
+
+ yes
+ yes
+ yes
+
+
+
+
+ Change another user’s DNS servers
+ Authentication is required to change another user’s DNS servers.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
+
+ Read another user’s DNS servers
+ Authentication is required to read another user’s DNS servers.
+
+ auth_admin_keep
+ auth_admin_keep
+ auth_admin_keep
+
+
+
Change your own session limits
Authentication is required to change your session limits.
diff --git a/accounts-service/com.endlessm.ParentalControls.rules.in b/accounts-service/com.endlessm.ParentalControls.rules.in
index fa021fa..be6a3b6 100644
--- a/accounts-service/com.endlessm.ParentalControls.rules.in
+++ b/accounts-service/com.endlessm.ParentalControls.rules.in
@@ -24,6 +24,8 @@ polkit.addRule(function(action, subject) {
* needing an additional polkit authorisation dialogue. */
if ((action.id == "com.endlessm.ParentalControls.AppFilter.ReadOwn" ||
action.id == "com.endlessm.ParentalControls.AppFilter.ReadAny" ||
+ action.id == "com.endlessm.ParentalControls.Dns.ReadOwn" ||
+ action.id == "com.endlessm.ParentalControls.Dns.ReadAny" ||
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadOwn" ||
action.id == "com.endlessm.ParentalControls.SessionLimits.ReadAny") &&
subject.active && subject.local &&
diff --git a/accounts-service/meson.build b/accounts-service/meson.build
index 198692c..967cfff 100644
--- a/accounts-service/meson.build
+++ b/accounts-service/meson.build
@@ -9,6 +9,7 @@ i18n.merge_file(
dbus_interfaces = [
'com.endlessm.ParentalControls.AccountInfo',
'com.endlessm.ParentalControls.AppFilter',
+ 'com.endlessm.ParentalControls.Dns',
'com.endlessm.ParentalControls.SessionLimits',
]
diff --git a/meson.build b/meson.build
index 4f0cabd..c43df29 100644
--- a/meson.build
+++ b/meson.build
@@ -1,4 +1,4 @@
-project('malcontent', 'c',
+project('malcontent', 'c', 'cpp',
version : '0.12.0',
meson_version : '>= 0.59.0',
license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'],
@@ -9,6 +9,10 @@ project('malcontent', 'c',
]
)
+if get_option('buildtype') in ['debugoptimized', 'debug']
+ add_project_arguments('-g3', language : ['c', 'cpp'])
+endif
+
gnome = import('gnome')
i18n = import('i18n')
pkgconfig = import('pkgconfig')
@@ -152,5 +156,10 @@ if get_option('ui').enabled()
update_desktop_database: true,
)
endif
+
+if get_option('nss').enabled()
+ subdir('nss')
+endif
+
subdir('pam')
subdir('po')
diff --git a/meson_options.txt b/meson_options.txt
index 726cac1..8f58ea3 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -9,6 +9,12 @@ option(
type: 'string',
description: 'directory for PAM modules'
)
+option(
+ 'nss',
+ type: 'feature',
+ value: 'enabled',
+ description: 'enable NSS module support (for parental DNS controls)'
+)
option(
'ui',
type: 'feature',
diff --git a/nss/cares_init.cc b/nss/cares_init.cc
new file mode 100644
index 0000000..3b5d855
--- /dev/null
+++ b/nss/cares_init.cc
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "cares_init.hh"
+
+#include
+
+#include
+#include
+
+using namespace std::literals;
+
+auto malcontent::CAresLibrary::instance() -> std::shared_ptr {
+ if (auto ret = _instance.lock()) {
+ return ret;
+ }
+
+ auto ret = std::make_shared(Private{});
+ _instance = ret;
+ return ret;
+}
+
+malcontent::CAresLibrary::CAresLibrary(Private) {
+ std::lock_guard guard { _init_mutex };
+ int ret = ares_library_init(ARES_LIB_INIT_ALL);
+ if (ret != ARES_SUCCESS) {
+ const auto err = "ares_library_init: "s + ares_strerror(ret);
+ throw std::system_error(ret, std::generic_category(), err.c_str());
+ }
+}
+
+malcontent::CAresLibrary::~CAresLibrary() noexcept {
+ std::lock_guard guard { _init_mutex };
+ ares_library_cleanup();
+}
+
+std::mutex malcontent::CAresLibrary::_init_mutex{};
+
+std::weak_ptr malcontent::CAresLibrary::_instance{};
diff --git a/nss/cares_init.hh b/nss/cares_init.hh
new file mode 100644
index 0000000..0df9092
--- /dev/null
+++ b/nss/cares_init.hh
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+
+namespace malcontent {
+
+class CAresLibrary {
+ struct Private {};
+
+public:
+ static auto instance() -> std::shared_ptr;
+
+ CAresLibrary(Private);
+ ~CAresLibrary() noexcept;
+
+ CAresLibrary(const CAresLibrary&) = delete;
+ CAresLibrary(CAresLibrary&&) = delete;
+
+private:
+ static std::mutex _init_mutex;
+ static std::weak_ptr _instance;
+};
+
+} // ~ namespace malcontent
diff --git a/nss/get_host.cc b/nss/get_host.cc
new file mode 100644
index 0000000..2bdb97f
--- /dev/null
+++ b/nss/get_host.cc
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "get_host.hh"
+#include "helpers.hh"
+#include "logger.hh"
+#include "user_policy.hh"
+
+namespace /* anonymous */ {
+
+static const malcontent::UserPolicy USER_POLICY;
+
+} // ~ namespace anonymous
+
+
+auto malcontent::get_host::by_name(ResolverArgs& args) -> nss_status {
+ set_if_valid(args.errnop, 0);
+ set_if_valid(args.h_errnop, HErrno::Success);
+
+ try {
+ auto resolver = USER_POLICY.resolver({});
+ if (!resolver) {
+ return nss_status::NSS_STATUS_NOTFOUND;
+ }
+ return resolver->resolve(args);
+ } catch(const std::exception& e) {
+ Logger::error(e.what());
+ return nss_status::NSS_STATUS_UNAVAIL;
+ }
+}
+
+auto malcontent::get_host::by_addr(
+ const void * /* addr */,
+ socklen_t /* len */,
+ int /* family */,
+ hostent * /* host */,
+ char * /* buffer */,
+ size_t /* buflen */,
+ int *errnop,
+ HErrno *h_errnop,
+ int32_t * /* ttlp */
+) -> nss_status {
+ set_if_valid(errnop, 0);
+ set_if_valid(h_errnop, HErrno::Success);
+
+ // At the moment, we are not handling this function
+ // in our module.
+ //
+ // We assume it is fine to go to the next module
+ // in the nsswitch.conf list to get an authoritative
+ // answer.
+ //
+ // The use case for reverse IP lookup
+ // should not impact parental controls.
+
+ return nss_status::NSS_STATUS_UNAVAIL;
+}
diff --git a/nss/get_host.hh b/nss/get_host.hh
new file mode 100644
index 0000000..17957da
--- /dev/null
+++ b/nss/get_host.hh
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "resolver.hh"
+
+namespace malcontent::get_host {
+
+auto by_name(ResolverArgs& args) -> nss_status;
+
+auto by_addr(
+ const void *addr,
+ socklen_t len,
+ int family,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop,
+ int32_t *ttlp
+) -> nss_status;
+
+} // ~ namespace malcontent::get_host
diff --git a/nss/helpers.cc b/nss/helpers.cc
new file mode 100644
index 0000000..a76ea89
--- /dev/null
+++ b/nss/helpers.cc
@@ -0,0 +1,122 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "helpers.hh"
+
+#include
+#include
+#include
+
+#include
+#include
+
+auto malcontent::copy_hostent(const hostent& src, hostent& dst, void *buffer, size_t buflen) -> void {
+ dst.h_addrtype = src.h_addrtype;
+ dst.h_length = src.h_length;
+
+ // copy name
+ {
+ auto name_len = std::strlen(src.h_name) + 1;
+ buffer = std::align(alignof(char), name_len, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc{};
+ }
+
+ dst.h_name = static_cast(std::memcpy(buffer, src.h_name, name_len));
+ reinterpret_cast(buffer) += name_len;
+ buflen -= name_len;
+ }
+
+ // copy addresses
+ {
+ auto begin_it = src.h_addr_list;
+ auto end_it = begin_it;
+ for (; *end_it != nullptr; ++end_it);
+ auto n = std::distance(begin_it, end_it);
+
+ auto needs_bytes = (n + 1) * sizeof(char *);
+ buffer = std::align(alignof(char *), needs_bytes, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc {};
+ }
+ dst.h_addr_list = static_cast(buffer);
+ reinterpret_cast(buffer) += n + 1;
+
+ for (auto i = 0; i < n; ++i) {
+ buffer = std::align(alignof(char), src.h_length, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc {};
+ }
+
+ dst.h_addr_list[i] = static_cast(
+ std::memcpy(buffer, src.h_addr_list[i], src.h_length));
+ reinterpret_cast(buffer) += src.h_length;
+ buflen -= src.h_length;
+ }
+ dst.h_addr_list[n] = nullptr;
+ }
+
+ // copy aliases
+ {
+ auto begin_it = src.h_aliases;
+ auto end_it = begin_it;
+ for (; *end_it != nullptr; ++end_it);
+
+ auto n = std::distance(begin_it, end_it);
+ auto needs_bytes = (n + 1) * sizeof(char *);
+ buffer = std::align(alignof(char *), needs_bytes, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc {};
+ }
+
+ dst.h_aliases = static_cast(buffer);
+ reinterpret_cast(buffer) += n + 1;
+ buflen -= needs_bytes;
+
+ for (int i = 0; i < n; ++i) {
+ auto alias_len = strlen(*begin_it) + 1;
+ buffer = std::align(alignof(char), alias_len, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc{};
+ }
+
+ dst.h_addr_list[i] = static_cast(
+ std::memcpy(buffer, src.h_aliases[i], n));
+
+ reinterpret_cast(buffer) += alias_len;
+ buflen -= alias_len;
+ }
+ dst.h_aliases[n] = nullptr;
+ }
+}
+
+auto malcontent::copy_hostent_to_gaih_addrtuple(const hostent& src, gaih_addrtuple *head, void *& buffer, size_t& buflen) -> gaih_addrtuple * {
+ auto name_len = std::strlen(src.h_name) + 1;
+ buffer = std::align(alignof(char), name_len, buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc{};
+ }
+ auto namep = static_cast(std::memcpy(buffer, src.h_name, name_len));
+ reinterpret_cast(buffer) += name_len;
+ buflen -= name_len;
+
+ for (auto addr = src.h_addr_list; *addr != nullptr; ++addr) {
+ buffer = std::align(alignof(gaih_addrtuple), sizeof(gaih_addrtuple), buffer, buflen);
+ if (buffer == nullptr) {
+ throw std::bad_alloc{};
+ }
+
+ auto tuple = static_cast(buffer);
+
+ tuple->family = src.h_addrtype;
+ tuple->name = namep;
+ std::memcpy(tuple->addr, *addr, src.h_length);
+ tuple->next = head; // <-- We are reversing result order here, but it shouldn't be a problem, right?
+ tuple->scopeid = 0; // FIXME: I have no clue how to determine this
+ head = tuple;
+
+ buflen -= sizeof(gaih_addrtuple);
+ }
+
+ return head;
+}
diff --git a/nss/helpers.hh b/nss/helpers.hh
new file mode 100644
index 0000000..c861732
--- /dev/null
+++ b/nss/helpers.hh
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+
+#include
+
+template
+inline auto set_if_valid(T *ptr, T val) -> T * {
+ if (ptr != nullptr) {
+ *ptr = std::move(val);
+ }
+ return ptr;
+}
+
+namespace malcontent {
+auto copy_hostent(const hostent& src, hostent& dst, void *buffer, size_t buflen) -> void;
+auto copy_hostent_to_gaih_addrtuple(const hostent& src, gaih_addrtuple *head, void *& buffer, size_t& buflen) -> gaih_addrtuple *;
+} // ~ namespace malcontent
diff --git a/nss/logger.cc b/nss/logger.cc
new file mode 100644
index 0000000..5d7b294
--- /dev/null
+++ b/nss/logger.cc
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "logger.hh"
+
+#include
+#include
+#include
+
+#include
+#include
+
+namespace /* anonymous */ {
+auto get_log_priority() -> int {
+ auto level = getenv("NSS_MALCONTENT_LOG_LEVEL");
+ if (level != nullptr) {
+ return atoi(level);
+ }
+
+ #ifdef NDEBUG
+ return LOG_WARNING;
+ #else
+ return LOG_DEBUG;
+ #endif
+}
+} // ~ namespace anonymous
+
+std::unique_ptr malcontent::Logger::_instance = nullptr;
+std::shared_mutex malcontent::Logger::_instance_mtx = {};
+
+auto malcontent::Logger::debug(const std::string_view& msg) -> void {
+ instance().log(LOG_DEBUG, msg);
+}
+
+auto malcontent::Logger::info(const std::string_view& msg) -> void {
+ instance().log(LOG_INFO, msg);
+}
+
+auto malcontent::Logger::warn(const std::string_view& msg) -> void {
+ instance().log(LOG_WARNING, msg);
+}
+
+auto malcontent::Logger::error(const std::string_view& msg) -> void {
+ instance().log(LOG_ERR, msg);
+}
+
+auto malcontent::Logger::instance() -> Logger& {
+ {
+ std::shared_lock lock { _instance_mtx };
+ if (_instance) return *_instance;
+ }
+
+ // First invocation: construct lazily
+ std::unique_lock lock { _instance_mtx };
+ if (!_instance) {
+ _instance = std::make_unique();
+ }
+ return *_instance;
+}
+
+auto malcontent::Logger::log(int priority, std::string_view msg) -> void {
+ if (priority <= _max_log_priority) {
+ msg = msg.substr(0, std::numeric_limits::max()); // clamp to avoid malicious overflows
+ ::syslog(priority, "%.*s", static_cast(msg.length()), msg.data());
+ }
+}
+
+malcontent::Logger::Logger()
+ : _max_log_priority(get_log_priority())
+{
+ ::openlog("nss_malcontent", LOG_PID | LOG_CONS, LOG_AUTHPRIV);
+}
+
+malcontent::Logger::~Logger() noexcept {
+ ::closelog();
+}
diff --git a/nss/logger.hh b/nss/logger.hh
new file mode 100644
index 0000000..be45fec
--- /dev/null
+++ b/nss/logger.hh
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+
+namespace malcontent {
+
+class Logger {
+ friend std::unique_ptr std::make_unique();
+ friend std::default_delete;
+
+public:
+ static auto debug(const std::string_view& msg) -> void;
+ static auto info(const std::string_view& msg) -> void;
+ static auto warn(const std::string_view& msg) -> void;
+ static auto error(const std::string_view& msg) -> void;
+
+private:
+ Logger();
+ Logger(const Logger&) = delete;
+ Logger(Logger&&) = delete;
+ ~Logger() noexcept;
+
+ auto operator=(const Logger&) = delete;
+ auto operator=(Logger&&) = delete;
+
+ auto log(int priority, std::string_view msg) -> void;
+
+ int _max_log_priority;
+
+ static auto instance() -> Logger&;
+
+ static std::unique_ptr _instance;
+ static std::shared_mutex _instance_mtx;
+};
+
+} // ~ namespace malcontent
diff --git a/nss/meson.build b/nss/meson.build
new file mode 100644
index 0000000..2b257a5
--- /dev/null
+++ b/nss/meson.build
@@ -0,0 +1,76 @@
+# SPDX-FileCopyrightText: Matteo Settenvini
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+gnome = import('gnome')
+gdbus_src = gnome.gdbus_codegen('parentalcontrol-dns',
+ sources: '../accounts-service/com.endlessm.ParentalControls.Dns.xml',
+ interface_prefix : 'com.endlessm.',
+ namespace : '',
+)
+
+source_files = [
+ 'cares_init.hh',
+ 'cares_init.cc',
+ 'get_host.hh',
+ 'get_host.cc',
+ 'helpers.hh',
+ 'helpers.cc',
+ 'logger.hh',
+ 'logger.cc',
+ 'nss_adapters.cc',
+ 'resolver.hh',
+ 'resolver.cc',
+ 'user_policy.hh',
+ 'user_policy.cc',
+ 'wrapper.hh',
+ gdbus_src,
+]
+
+standard_args = [
+ '-Wall',
+ '-Wextra',
+ '-pedantic',
+]
+
+dependencies = [
+ dependency('libcares', version: '>= 1.19.1'),
+ dependency('gio-2.0', version: '>= 2.26'),
+]
+
+standards = ['cpp_std=c++20']
+
+shared_library('nss_malcontent',
+ sources: source_files,
+ cpp_args: standard_args,
+ gnu_symbol_visibility: 'hidden',
+ extra_files: [
+ '../accounts-service/com.endlessm.ParentalControls.Dns.xml',
+ '../accounts-service/com.endlessm.ParentalControls.policy.in',
+ '../accounts-service/com.endlessm.ParentalControls.rules.in',
+ ],
+ override_options: standards,
+ soversion: '2',
+ install: true,
+ dependencies: dependencies,
+)
+
+test_source_files = [
+ 'test_main.cc',
+ 'test_resolver.cc',
+ 'testsuite.hh',
+]
+
+nss_tests = executable('nss_malcontent-tests',
+ sources: source_files + test_source_files,
+ cpp_args: ['-DNSS_MALCONTENT_TEST'],
+ override_options : standards,
+ dependencies: dependencies + [ dependency('glib-2.0', version: '>= 2.74') ])
+
+test('nss_malcontent',
+ nss_tests,
+ env: [
+ 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()),
+ 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()),
+ ],
+ protocol: 'tap',
+)
diff --git a/nss/nss_adapters.cc b/nss/nss_adapters.cc
new file mode 100644
index 0000000..353f6cc
--- /dev/null
+++ b/nss/nss_adapters.cc
@@ -0,0 +1,214 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "get_host.hh"
+#include "helpers.hh"
+#include "logger.hh"
+#include "resolver.hh"
+#include "wrapper.hh"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#define DSO_EXPORT extern "C" __attribute__((visibility("default")))
+
+// -------------- by host ---------------
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyname4_r(
+ const char *name,
+ gaih_addrtuple **pat,
+ char *buffer_c,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop,
+ int32_t *ttlp
+) -> nss_status {
+ try {
+ void *buffer = buffer_c;
+ hostent he;
+ auto local_buffer = std::make_unique(buflen);
+
+ auto args = malcontent::ResolverArgs {
+ name,
+ 0,
+ &he,
+ local_buffer.get(),
+ buflen,
+ errnop,
+ h_errnop,
+ ttlp
+ };
+
+ args.family = AF_INET;
+ auto ret_a = malcontent::get_host::by_name(args);
+ if (ret_a != NSS_STATUS_SUCCESS || pat == nullptr) {
+ return ret_a;
+ }
+ *pat = malcontent::copy_hostent_to_gaih_addrtuple(he, nullptr, buffer, buflen);
+
+ args.family = AF_INET6;
+ auto ret_a6 = malcontent::get_host::by_name(args);
+ if (ret_a6 != NSS_STATUS_SUCCESS) {
+ return ret_a6;
+ }
+ *pat = malcontent::copy_hostent_to_gaih_addrtuple(he, *pat, buffer, buflen);
+ set_if_valid(ttlp, 0); // We don't know which one to keep, so 0.
+ return NSS_STATUS_SUCCESS;
+
+ } catch(const std::bad_alloc&) {
+ set_if_valid(errnop, ERANGE);
+ set_if_valid(h_errnop, HErrno::Internal);
+ return nss_status::NSS_STATUS_TRYAGAIN;
+
+ } catch(const std::exception& e) {
+ malcontent::Logger::error(std::string(__func__) + e.what());
+ set_if_valid(h_errnop, HErrno::Internal);
+ return nss_status::NSS_STATUS_TRYAGAIN;
+ }
+}
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyname3_r(
+ const char *name,
+ int af,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop,
+ int32_t *ttlp,
+ char **canonp
+) -> nss_status {
+ try {
+ auto args = malcontent::ResolverArgs {
+ name,
+ af,
+ host,
+ buffer,
+ buflen,
+ errnop,
+ h_errnop,
+ ttlp
+ };
+
+ auto status = malcontent::get_host::by_name(args);
+ if (host != nullptr) {
+ set_if_valid(canonp, host->h_name);
+ }
+ return status;
+
+ } catch(const std::exception& e) {
+ malcontent::Logger::error(std::string(__func__) + e.what());
+ set_if_valid(h_errnop, HErrno::Internal);
+ return nss_status::NSS_STATUS_TRYAGAIN;
+ }
+}
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyname2_r(
+ const char *name,
+ int af,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop
+) -> nss_status {
+ return _nss_malcontent_gethostbyname3_r(
+ name,
+ af,
+ host,
+ buffer,
+ buflen,
+ errnop,
+ h_errnop,
+ nullptr,
+ nullptr
+ );
+}
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyname_r(
+ const char *name,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop
+) -> nss_status {
+ return _nss_malcontent_gethostbyname3_r(
+ name,
+ AF_INET,
+ host,
+ buffer,
+ buflen,
+ errnop,
+ h_errnop,
+ nullptr,
+ nullptr
+ );
+}
+
+// ----------------- by addr -----------------
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyaddr2_r(
+ const void *addr,
+ socklen_t len,
+ int af,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop,
+ int32_t *ttlp
+) -> nss_status {
+ try {
+ return malcontent::get_host::by_addr(
+ addr,
+ len,
+ af,
+ host,
+ buffer,
+ buflen,
+ errnop,
+ h_errnop,
+ ttlp
+ );
+ } catch(const std::exception& e) {
+ malcontent::Logger::error(std::string(__func__) + e.what());
+ set_if_valid(h_errnop, HErrno::Internal);
+ return nss_status::NSS_STATUS_TRYAGAIN;
+ }
+}
+
+DSO_EXPORT
+auto _nss_malcontent_gethostbyaddr_r(
+ const void *addr,
+ socklen_t len,
+ int af,
+ hostent *host,
+ char *buffer,
+ size_t buflen,
+ int *errnop,
+ HErrno *h_errnop
+) -> nss_status {
+ return _nss_malcontent_gethostbyaddr2_r(
+ addr,
+ len,
+ af,
+ host,
+ buffer,
+ buflen,
+ errnop,
+ h_errnop,
+ nullptr
+ );
+}
diff --git a/nss/resolver.cc b/nss/resolver.cc
new file mode 100644
index 0000000..24de3f2
--- /dev/null
+++ b/nss/resolver.cc
@@ -0,0 +1,304 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "resolver.hh"
+
+#include "cares_init.hh"
+#include "helpers.hh"
+#include "logger.hh"
+#include "wrapper.hh"
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::literals;
+
+namespace /* anonymous */ {
+
+constexpr const auto CANARY_HOSTNAME = "use-application-dns.net"sv;
+
+auto init_channel(const std::vector dns) -> ares_channel_t *;
+auto setup_servers(ares_channel_t *channel, const std::vector dns) -> void;
+auto parse_address(const std::string& addr, ares_addr_port_node& into) -> void;
+
+struct CAresAddrListDeletor {
+ auto operator()(ares_addr_port_node *list) const -> void {
+ if(list == nullptr) {
+ return;
+ }
+ operator()(list->next);
+ delete list;
+ }
+};
+
+using CAresAddrList = std::unique_ptr;
+
+struct CallbackArgs {
+ malcontent::Resolver *resolver;
+ malcontent::ResolverArgs *args;
+ nss_status return_status = NSS_STATUS_TRYAGAIN;
+};
+
+} // ~ anonymous namespace
+
+malcontent::Resolver::Resolver(std::vector dns)
+ : _ensure_cares_initialized(CAresLibrary::instance())
+ , _channel(init_channel(std::move(dns)))
+{
+}
+
+auto malcontent::Resolver::resolve(ResolverArgs& args) -> nss_status {
+ if (args.name == CANARY_HOSTNAME) {
+ // disable DoH if user did not explicitly turn it on
+ set_if_valid(args.errnop, 0);
+ set_if_valid(args.h_errnop, HErrno::HostNotFound);
+ return NSS_STATUS_SUCCESS;
+ }
+
+ ns_type type;
+ switch(args.family) {
+ case AF_INET: {
+ type = ns_t_a;
+ break;
+ }
+ case AF_INET6: {
+ type = ns_t_a6;
+ break;
+ }
+ default: {
+ throw std::invalid_argument("only AF_INET and AF_INET6 are supported families");
+ }
+ }
+
+ CallbackArgs closure { .resolver = this, .args = &args };
+ ares_query(_channel.get(), args.name, ns_c_in, type, &Resolver::resolve_cb, &closure);
+
+ fd_set readers, writers;
+ FD_ZERO(&readers);
+ FD_ZERO(&writers);
+ int nfds = ares_fds(_channel.get(), &readers, &writers);
+
+ int epollfd = epoll_create1(EPOLL_CLOEXEC);
+ if (epollfd == -1) {
+ throw std::system_error{ errno, std::system_category(), "epoll_create1" };
+ }
+
+ // translate from obsolete select() to epoll(),
+ // as calling process might use a big number
+ // of file descriptors.
+ epoll_event ev;
+ for (int& i = ev.data.fd = 0; i < nfds; ++i) {
+ if (FD_ISSET(i, &readers)) {
+ ev.events = EPOLLIN;
+ } else if (FD_ISSET(i, &writers)) {
+ ev.events = EPOLLOUT;
+ } else {
+ continue;
+ }
+
+ if (epoll_ctl(epollfd, EPOLL_CTL_ADD, i, &ev) == -1) {
+ throw std::system_error{ errno, std::system_category(), "epoll_ctl" };
+ }
+ }
+
+ timeval tv;
+ while (true) {
+ epoll_event ev;
+ int timeout_ms = 0;
+ auto tvp = ares_timeout(_channel.get(), nullptr, &tv);
+ if (tvp != nullptr) {
+ timeout_ms = tvp->tv_sec * 1000 + tvp->tv_usec / 1000;
+ }
+
+ auto nfds = epoll_wait(epollfd, &ev, 1, timeout_ms);
+ if (nfds == -1) {
+ throw std::system_error{ errno, std::system_category(), "epoll_wait" };
+ } else if (nfds == 0) {
+ // timeout or end of processing
+ break;
+ }
+
+ if (ev.events & EPOLLIN) {
+ ares_process_fd(_channel.get(), ev.data.fd, ARES_SOCKET_BAD);
+ } else if (ev.events & EPOLLOUT) {
+ ares_process_fd(_channel.get(), ARES_SOCKET_BAD, ev.data.fd);
+ }
+ }
+
+ return closure.return_status;
+}
+
+auto malcontent::Resolver::resolve_cb(void *arg,
+ int status,
+ int timeouts,
+ unsigned char *abuf,
+ int alen) -> void {
+ auto& closure = *static_cast(arg);
+ closure.return_status = closure.resolver->resolved(*closure.args, status, timeouts, abuf, alen);
+}
+
+auto malcontent::Resolver::resolved(ResolverArgs& args,
+ int status,
+ int /*timeouts*/,
+ unsigned char *abuf,
+ int alen) -> nss_status {
+ using std::swap;
+
+ switch (status) {
+ case ARES_SUCCESS: {
+ hostent *results = nullptr;
+ int parse_ret;
+ switch(args.family) {
+ case AF_INET: {
+ int n_ttls = 1;
+ ares_addrttl ttl;
+ parse_ret = ares_parse_a_reply(abuf, alen, &results, &ttl, &n_ttls);
+ set_if_valid(args.ttlp, n_ttls == 1 ? ttl.ttl : 0);
+ break;
+ }
+ case AF_INET6: {
+ int n_ttls = 1;
+ ares_addr6ttl ttl;
+ parse_ret = ares_parse_aaaa_reply(abuf, alen, &results, &ttl, &n_ttls);
+ set_if_valid(args.ttlp, n_ttls == 1 ? ttl.ttl : 0);
+ break;
+ }
+ default: {
+ throw std::invalid_argument("only AF_INET and AF_INET6 are supported families");
+ }
+ }
+
+ if (parse_ret != ARES_SUCCESS) {
+ set_if_valid(args.errnop, EAGAIN);
+ set_if_valid(args.h_errnop, HErrno::Internal);
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ try {
+ copy_hostent(*results, *args.result, args.buffer, args.buflen);
+ ares_free_hostent(results);
+ } catch (const std::bad_alloc&) {
+ // buffer is too small
+ ares_free_hostent(results);
+ set_if_valid(args.errnop, ERANGE);
+ set_if_valid(args.h_errnop, HErrno::Internal);
+ return NSS_STATUS_TRYAGAIN;
+ }
+
+ set_if_valid(args.errnop, 0);
+ set_if_valid(args.h_errnop, HErrno::Success);
+ return NSS_STATUS_SUCCESS;
+ }
+
+ case ARES_ENOTFOUND: {
+ set_if_valid(args.errnop, 0);
+ set_if_valid(args.h_errnop, HErrno::HostNotFound);
+ return NSS_STATUS_SUCCESS;
+ }
+
+ case ARES_ENODATA: {
+ set_if_valid(args.errnop, 0);
+ set_if_valid(args.h_errnop, HErrno::NoData);
+ return NSS_STATUS_SUCCESS;
+ }
+
+ case ARES_ETIMEOUT: {
+ set_if_valid(args.errnop, EAGAIN);
+ set_if_valid(args.h_errnop, HErrno::TryAgain);
+ return NSS_STATUS_SUCCESS;
+ }
+
+ case ARES_ECANCELLED:
+ case ARES_EDESTRUCTION:
+ case ARES_ENOMEM:
+ default: {
+ set_if_valid(args.errnop, EAGAIN);
+ set_if_valid(args.h_errnop, HErrno::Internal);
+ return NSS_STATUS_TRYAGAIN;
+ }
+ }
+}
+
+namespace /* anonymous */ {
+
+auto init_channel(const std::vector dns) -> ares_channel_t * {
+ ares_channel_t *channel = nullptr;
+ ares_init(&channel);
+ setup_servers(channel, std::move(dns));
+ return channel;
+}
+
+auto setup_servers(ares_channel_t *channel, const std::vector dns) -> void {
+ CAresAddrList list;
+ for (auto it = dns.crbegin(); it != dns.crend(); ++it) {
+ auto new_node = std::make_unique();
+ try {
+ parse_address(*it, *new_node);
+ auto new_node_unsafe = new_node.release();
+ new_node_unsafe->next = list.release();
+ list.reset(new_node_unsafe);
+ malcontent::Logger::debug(std::format("adding {} to the list of user DNS resolvers", *it));
+ } catch (const std::exception& e) {
+ malcontent::Logger::error(e.what());
+ throw;
+ }
+ }
+
+ int ret = ares_set_servers_ports(channel, list.get());
+ if (ret != ARES_SUCCESS) {
+ const auto err = std::string("ares_set_server_ports: ") + ares_strerror(ret);
+ throw std::system_error(ret, std::generic_category(), err.c_str());
+ }
+}
+
+auto parse_address(const std::string& addr, ares_addr_port_node& into) -> void {
+ static const auto ADDR4_REGEX = std::regex{ R"(([0-9\.]+)(?::([0-9]+))?(?:#(.*))?)" };
+ static const auto ADDR6_REGEX = std::regex{ R"((?:([[0-9a-f:]+)|\[([0-9a-f:]+)\]:([0-9]+)?)(?:#(.*))?)" };
+ try {
+ std::smatch matches;
+ size_t ip_idx, port_idx, host_idx;
+
+ if (std::regex_match(addr, matches, ADDR4_REGEX)) {
+ into.family = AF_INET;
+ ip_idx = 1;
+ port_idx = 2;
+ host_idx = 3;
+ } else if (std::regex_match(addr, matches, ADDR6_REGEX)) {
+ into.family = AF_INET6;
+ ip_idx = matches[1].matched ? 1 : 2;
+ port_idx = 3;
+ host_idx = 4;
+ } else {
+ throw std::invalid_argument{"expecting '[:port][#hostname]'"};
+ }
+
+ if (ares_inet_pton(into.family, matches[ip_idx].str().c_str(), &into.addr) <= 0) {
+ throw std::system_error {errno, std::system_category()};
+ }
+
+ if (matches[host_idx].matched) { // hostname -> TLS
+ // FIXME: as of now we are ignoring the hostname verification
+ into.tcp_port = matches[port_idx].matched ? stoi(matches[port_idx]) : 853;
+ } else { // no hostname -> no TLS
+ into.udp_port = matches[port_idx].matched ? stoi(matches[port_idx]) : 53;
+ }
+ } catch (const std::exception& e) {
+ throw std::invalid_argument("unable to parse DNS server address '" + addr + "': " + e.what());
+ }
+}
+
+} // ~ namespace anonymous
diff --git a/nss/resolver.hh b/nss/resolver.hh
new file mode 100644
index 0000000..e94ea4e
--- /dev/null
+++ b/nss/resolver.hh
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "wrapper.hh"
+
+#include "cares_init.hh"
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace malcontent {
+
+struct ResolverArgs {
+ const char *name; // in
+ int family; // in
+ hostent *result; // out
+ char *buffer; // out
+ size_t buflen; // in
+ int *errnop; // out
+ HErrno *h_errnop; // out
+ int32_t *ttlp; // out
+};
+
+class Resolver {
+public:
+ Resolver(std::vector dns);
+
+ auto resolve(ResolverArgs& args) -> nss_status;
+
+private:
+ struct CAresDeletor {
+ auto operator() (ares_channel_t *ch) const -> void {
+ ares_destroy(ch);
+ }
+ };
+
+ using CAresChannel = std::unique_ptr;
+
+ static auto resolve_cb(void *arg,
+ int status,
+ int timeouts,
+ unsigned char *abuf,
+ int alen) -> void;
+
+ auto resolved(ResolverArgs& result,
+ int status,
+ int timeouts,
+ unsigned char *abuf,
+ int alen) -> nss_status;
+
+ std::shared_ptr _ensure_cares_initialized;
+ CAresChannel _channel;
+};
+
+} // ~ namespace malcontent
diff --git a/nss/test_main.cc b/nss/test_main.cc
new file mode 100644
index 0000000..17949f6
--- /dev/null
+++ b/nss/test_main.cc
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "testsuite.hh"
+
+#include
+
+#include
+
+
+auto main(int argc, char *argv[]) -> int
+{
+ setenv("NSS_MALCONTENT_LOG_LEVEL", "7", 1);
+ setlocale(LC_ALL, "");
+ g_test_init(&argc, &argv, NULL);
+ g_test_set_nonfatal_assertions();
+
+ define_resolver_tests();
+
+ return g_test_run();
+}
diff --git a/nss/test_resolver.cc b/nss/test_resolver.cc
new file mode 100644
index 0000000..1bb2c3a
--- /dev/null
+++ b/nss/test_resolver.cc
@@ -0,0 +1,192 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "testsuite.hh"
+
+#include "resolver.hh"
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace /* anonymous */ {
+
+auto resolver_valid_dns_address(gconstpointer addr_cstr) -> void;
+auto resolver_invalid_dns_address(gconstpointer addr_cstr) -> void;
+auto resolver_dns0eu_blocks_unsafe_domains(gconstpointer should_be_blocked_cstr) -> void;
+auto resolver_cloudflare_family_blocks_unsafe_domains(gconstpointer should_be_blocked_cstr) -> void;
+
+} // ~ namespace anonymous
+
+auto define_resolver_tests() -> void {
+ auto valid_dns_addrs = std::array {
+ "192.168.0.1",
+ "192.168.0.1:53",
+ "192.168.0.1#dns.example.com",
+ "192.168.0.1:53#dns.example.com",
+ "192.168.0.1#",
+ "2345:0425:2ca1:0000:0000:0567:5673:23b5",
+ "2345:425:2ca1::567:5673:23b5",
+ "[2345::5673:23b5]:63",
+ "[2345::5673:23b5]:63#example.com",
+ "2345::5673:23b5#example.com",
+ "[2345::5673:23b5]:",
+ "[2345::5673:23b5]:#foo.bar",
+ "2345::5673:23b5#",
+ };
+
+ for (auto addr: valid_dns_addrs) {
+ g_test_add_data_func(std::format("/resolver/parses-valid-dns/{}", addr).c_str(),
+ addr, resolver_valid_dns_address);
+ }
+
+ auto invalid_dns_addrs = std::array {
+ "192.168.0.1.1",
+ "192.168.0.256",
+ "192.168..20",
+ "-192.168.0.1",
+ "192.168.0.1:",
+ "192.168.0.1:#foo.bar",
+ "2345:0425:2ca1:0000:0000:0567:5673:23b5:2020:2021",
+ "2345::5673::23b5",
+ "2345::5673:ghij",
+ "2345::5673:23b5:",
+ };
+
+ for (auto addr: invalid_dns_addrs) {
+ g_test_add_data_func(std::format("/resolver/rejects-invalid-dns/{}", addr).c_str(),
+ addr, resolver_invalid_dns_address);
+ }
+
+ // dns0.eu seems fairly good as blockfilters go
+ auto should_be_blocked_dns0 = std::array {
+ "pornhub.com",
+ "bookmaker.com",
+ "thepiratebay.org",
+ };
+
+ for (auto hostname : should_be_blocked_dns0) {
+ g_test_add_data_func(std::format("/resolver/kids.dns0.eu/blocks/{}", hostname).c_str(),
+ hostname, resolver_dns0eu_blocks_unsafe_domains);
+ }
+
+ // Cloudflare family seems far less restrictive
+ auto should_be_blocked_cff = std::array {
+ "pornhub.com",
+ "thepiratebay.org",
+ };
+
+ for (auto hostname : should_be_blocked_cff) {
+ g_test_add_data_func(std::format("/resolver/family.cloudflare-dns.com/blocks/{}", hostname).c_str(),
+ hostname, resolver_cloudflare_family_blocks_unsafe_domains);
+ }
+}
+
+namespace /* anonymous */ {
+auto resolver_valid_dns_address(gconstpointer addr_cstr) -> void {
+ auto addr = static_cast(addr_cstr);
+ try {
+ malcontent::Resolver resolver({ addr });
+
+ } catch (const std::exception& e) {
+ g_assert_unreachable(e.what());
+ }
+}
+
+auto resolver_invalid_dns_address(gconstpointer addr_cstr) -> void {
+ auto addr = static_cast(addr_cstr);
+ try {
+ malcontent::Resolver resolver({ addr });
+ auto error = std::format("DNS addr parsing did not fail as expected for {}", addr);
+ g_assert_unreachable(error.c_str());
+
+ } catch (const std::invalid_argument& e) {
+ g_assert_true(std::string(e.what()).find("unable to parse DNS server address") != std::string::npos);
+
+ } catch (const std::exception& e) {
+ g_assert_unreachable(e.what());
+ }
+}
+
+auto resolver_dns0eu_blocks_unsafe_domains(gconstpointer should_be_blocked_cstr) -> void {
+ auto should_be_blocked = static_cast(should_be_blocked_cstr);
+ try {
+ malcontent::Resolver resolver({
+ "193.110.81.1#kids.dns0.eu",
+ "2a0f:fc80::1#kids.dns0.eu",
+ "185.253.5.1#kids.dns0.eu",
+ "2a0f:fc81::1#kids.dns0.eu",
+ });
+
+
+ hostent result;
+ int local_errno{};
+ HErrno local_herrno{};
+ malcontent::ResolverArgs args {
+ .name = should_be_blocked,
+ .family = AF_INET,
+ .result = &result,
+ .buffer = nullptr,
+ .buflen = 0,
+ .errnop = &local_errno,
+ .h_errnop = &local_herrno,
+ .ttlp = nullptr,
+ };
+
+ resolver.resolve(args);
+ g_assert_cmpint(local_errno, ==, 0);
+ g_assert_true(local_herrno == HErrno::HostNotFound);
+
+ } catch (const std::exception& e) {
+ g_assert_unreachable(e.what());
+ }
+}
+
+auto resolver_cloudflare_family_blocks_unsafe_domains(gconstpointer should_be_blocked_cstr) -> void {
+ auto should_be_blocked = static_cast(should_be_blocked_cstr);
+
+ const char NULL_NULL_NULL_NULL[] = { 0x0, 0x0, 0x0, 0x0 };
+ try {
+ malcontent::Resolver resolver({
+ "1.1.1.3#family.cloudflare-dns.com",
+ "1.0.0.3#family.cloudflare-dns.com",
+ "2606:4700:4700::1113#family.cloudflare-dns.com",
+ "2606:4700:4700::1003#family.cloudflare-dns.com",
+ });
+
+ constexpr size_t BUFLEN = 2056;
+ char buffer[BUFLEN];
+ hostent result;
+ int local_errno{};
+ HErrno local_herrno{};
+ malcontent::ResolverArgs args {
+ .name = should_be_blocked,
+ .family = AF_INET,
+ .result = &result,
+ .buffer = buffer,
+ .buflen = BUFLEN,
+ .errnop = &local_errno,
+ .h_errnop = &local_herrno,
+ .ttlp = nullptr,
+ };
+
+ resolver.resolve(args);
+ g_assert_cmpint(local_errno, ==, 0);
+ g_assert_true(local_herrno == HErrno::Success);
+ g_assert_cmpint(result.h_addrtype, ==, AF_INET);
+ g_assert_cmpint(result.h_length, ==, 4);
+ g_assert_nonnull(result.h_addr_list);
+ g_assert_cmpmem(*result.h_addr_list, result.h_length, NULL_NULL_NULL_NULL, 4);
+ g_assert_null(result.h_addr_list[1]);
+
+ } catch (const std::exception& e) {
+ g_assert_unreachable(e.what());
+ }
+}
+
+// TODO: TEST canary
+
+} // ~ namespace anonymous
diff --git a/nss/testsuite.hh b/nss/testsuite.hh
new file mode 100644
index 0000000..10e62a4
--- /dev/null
+++ b/nss/testsuite.hh
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+
+#define g_assert_unreachable(msg) g_assert_cmpstr(msg, ==, nullptr)
+
+auto define_resolver_tests() -> void;
diff --git a/nss/user_policy.cc b/nss/user_policy.cc
new file mode 100644
index 0000000..8fedb5c
--- /dev/null
+++ b/nss/user_policy.cc
@@ -0,0 +1,78 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "user_policy.hh"
+
+#include "logger.hh"
+#include "resolver.hh"
+
+#include "parentalcontrol-dns.h"
+
+#include
+#include
+#include
+
+#include
+
+namespace /* anonymous */ {
+
+struct GObjectDeletor {
+ auto operator() (void *gobj) const noexcept -> void {
+ g_object_unref(gobj);
+ }
+};
+
+} // ~ namespace anonymous
+
+
+using ProxyPtr = std::unique_ptr;
+
+auto malcontent::UserPolicy::resolver(std::optional maybe_uid) const -> std::shared_ptr {
+ auto uid = maybe_uid.value_or(getuid());
+ if (uid == 0) {
+ return nullptr; // no restrictions for root!
+ }
+
+ // If cached, let's return this immediately
+ {
+ std::shared_lock lock { _cache_mutex };
+ auto it = _resolver_cache.find(uid);
+ if (it != _resolver_cache.cend()) {
+ return it->second;
+ }
+ }
+
+ // Missing from cache, insert if still needed
+ std::unique_lock lock { _cache_mutex };
+ auto it = _resolver_cache.find(uid);
+ if (it != _resolver_cache.cend()) {
+ return it->second;
+ }
+
+ GError *error = nullptr;
+ auto dbus_proxy = ProxyPtr {
+ parental_controls_dns_proxy_new_for_bus_sync(
+ G_BUS_TYPE_SYSTEM,
+ // right now we check the property value at start
+ // and we never update during runtime.
+ G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
+ "com.endlessm.ParentalControls.Dns",
+ "/com/endlessm/ParentalControls/Dns",
+ nullptr,
+ &error
+ )
+ };
+
+ if (error != nullptr) {
+ malcontent::Logger::warn(error->message);
+ return nullptr;
+ }
+
+ std::vector dns;
+ for (auto it = parental_controls_dns_get_dns(dbus_proxy.get()); it != nullptr; ++it) {
+ dns.push_back(*it);
+ }
+
+ it = _resolver_cache.insert(it, { uid, std::make_shared(std::move(dns)) });
+ return it->second;
+}
diff --git a/nss/user_policy.hh b/nss/user_policy.hh
new file mode 100644
index 0000000..dae7e61
--- /dev/null
+++ b/nss/user_policy.hh
@@ -0,0 +1,31 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+
+namespace malcontent {
+
+class Resolver;
+
+class UserPolicy {
+public:
+ UserPolicy() = default;
+ UserPolicy(const UserPolicy&) = delete;
+ UserPolicy(UserPolicy&&) = delete;
+ ~UserPolicy() noexcept = default;
+
+ auto resolver(std::optional uid = {}) const -> std::shared_ptr;
+
+private:
+ mutable std::unordered_map> _resolver_cache;
+ mutable std::shared_mutex _cache_mutex;
+};
+
+} // ~ namespace malcontent
diff --git a/nss/wrapper.hh b/nss/wrapper.hh
new file mode 100644
index 0000000..977e88d
--- /dev/null
+++ b/nss/wrapper.hh
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Matteo Settenvini
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include
+#include
+
+// Work around enums in netdb.h defined as macros instead :-p
+
+enum class HErrno : int {
+ Success = 0,
+ HostNotFound = HOST_NOT_FOUND,
+ TryAgain = TRY_AGAIN,
+ NoRecovery = NO_RECOVERY,
+ NoData = NO_DATA,
+ #ifdef __USE_MISC
+ Internal = NETDB_INTERNAL,
+ #endif
+};
+
+enum class EaiRetcode : int {
+ Success = 0,
+ BadFlags = EAI_BADFLAGS,
+ NoName = EAI_NONAME,
+ Fail = EAI_FAIL,
+ Family = EAI_FAMILY,
+ SockType = EAI_SOCKTYPE,
+ Service = EAI_SERVICE,
+ Memory = EAI_MEMORY,
+ System = EAI_SYSTEM,
+ Overflow = EAI_OVERFLOW,
+ #ifdef __USE_GNU
+ NoData = EAI_NODATA,
+ AddrFamily = EAI_ADDRFAMILY,
+ InProgress = EAI_INPROGRESS,
+ Canceled = EAI_CANCELED,
+ NotCanceled = EAI_NOTCANCELED,
+ AllDone = EAI_ALLDONE,
+ Interrupted = EAI_INTR,
+ IdnEncode = EAI_IDN_ENCODE,
+ #endif /* __USE_GNU */
+};