Compare commits

..

2 Commits

Author SHA1 Message Date
Matteo Settenvini 596b5f7f5a
dns: c++ implementation of nss module 2024-01-05 18:10:45 +01:00
Matteo Settenvini 3beee1cf84
dns: add dbus interfaces 2024-01-05 17:55:52 +01:00
24 changed files with 1538 additions and 1 deletions

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="com.endlessm.ParentalControls.Dns">
<annotation name="org.freedesktop.Accounts.VendorExtension" value="true"/>
<annotation name="org.freedesktop.Accounts.Authentication.ChangeOwn"
value="com.endlessm.ParentalControls.SessionLimits.ChangeOwn"/>
<annotation name="org.freedesktop.Accounts.Authentication.ReadOwn"
value="com.endlessm.ParentalControls.SessionLimits.ReadOwn"/>
<annotation name="org.freedesktop.Accounts.Authentication.ChangeAny"
value="com.endlessm.ParentalControls.SessionLimits.ChangeAny"/>
<annotation name="org.freedesktop.Accounts.Authentication.ReadAny"
value="com.endlessm.ParentalControls.SessionLimits.ReadAny"/>
<!--
Dns:
A list of DNS server IP addresses to use for this user, in order of preference.
Each can be optionally qualified by a hashtag and the corresponding
hostname (if a TLS version is supported).
For instance: using "dns0.eu Kids" (https://www.dns0.eu/kids),
which offers a good number of blocked domains, would entail:
- `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`
If the array is left empty, the globally-set system resolver
is used instead.
-->
<property name="Dns" type="as" access="readwrite">
<annotation name="org.freedesktop.Accounts.DefaultValue" value=""/>
</property>
</interface>
</node>

View File

@ -40,6 +40,46 @@
</defaults>
</action>
<action id="com.endlessm.ParentalControls.Dns.ChangeOwn">
<description>Change your own DNS servers</description>
<message>Authentication is required to change your DNS servers.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="com.endlessm.ParentalControls.Dns.ReadOwn">
<description>Read your own DNS servers</description>
<message>Authentication is required to read your DNS servers.</message>
<defaults>
<allow_any>yes</allow_any>
<allow_inactive>yes</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
</action>
<action id="com.endlessm.ParentalControls.Dns.ChangeAny">
<description>Change another users DNS servers</description>
<message>Authentication is required to change another users DNS servers.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="com.endlessm.ParentalControls.Dns.ReadAny">
<description>Read another users DNS servers</description>
<message>Authentication is required to read another users DNS servers.</message>
<defaults>
<allow_any>auth_admin_keep</allow_any>
<allow_inactive>auth_admin_keep</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
<action id="com.endlessm.ParentalControls.SessionLimits.ChangeOwn">
<description>Change your own session limits</description>
<message>Authentication is required to change your session limits.</message>

View File

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

View File

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

View File

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

View File

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

39
nss/cares_init.cc Normal file
View File

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "cares_init.hh"
#include <ares.h>
#include <string>
#include <system_error>
using namespace std::literals;
auto malcontent::CAresLibrary::instance() -> std::shared_ptr<CAresLibrary> {
if (auto ret = _instance.lock()) {
return ret;
}
auto ret = std::make_shared<CAresLibrary>(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> malcontent::CAresLibrary::_instance{};

28
nss/cares_init.hh Normal file
View File

@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <mutex>
namespace malcontent {
class CAresLibrary {
struct Private {};
public:
static auto instance() -> std::shared_ptr<CAresLibrary>;
CAresLibrary(Private);
~CAresLibrary() noexcept;
CAresLibrary(const CAresLibrary&) = delete;
CAresLibrary(CAresLibrary&&) = delete;
private:
static std::mutex _init_mutex;
static std::weak_ptr<CAresLibrary> _instance;
};
} // ~ namespace malcontent

57
nss/get_host.cc Normal file
View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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;
}

24
nss/get_host.hh Normal file
View File

@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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

122
nss/helpers.cc Normal file
View File

@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "helpers.hh"
#include <new>
#include <nss.h>
#include <netdb.h>
#include <cstring>
#include <memory>
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<char *>(std::memcpy(buffer, src.h_name, name_len));
reinterpret_cast<char *&>(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<char **>(buffer);
reinterpret_cast<char **&>(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<char *>(
std::memcpy(buffer, src.h_addr_list[i], src.h_length));
reinterpret_cast<char *&>(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<char **>(buffer);
reinterpret_cast<char **&>(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<char *>(
std::memcpy(buffer, src.h_aliases[i], n));
reinterpret_cast<char *&>(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<char *>(std::memcpy(buffer, src.h_name, name_len));
reinterpret_cast<char *&>(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<gaih_addrtuple *>(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;
}

21
nss/helpers.hh Normal file
View File

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <nss.h>
#include <utility>
template <typename T>
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

76
nss/logger.cc Normal file
View File

@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "logger.hh"
#include <cstdlib>
#include <mutex>
#include <limits>
#include <sys/syslog.h>
#include <syslog.h>
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> 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<Logger>();
}
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<int>::max()); // clamp to avoid malicious overflows
::syslog(priority, "%.*s", static_cast<int>(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();
}

41
nss/logger.hh Normal file
View File

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <shared_mutex>
#include <string_view>
namespace malcontent {
class Logger {
friend std::unique_ptr<Logger> std::make_unique<Logger>();
friend std::default_delete<Logger>;
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<Logger> _instance;
static std::shared_mutex _instance_mtx;
};
} // ~ namespace malcontent

76
nss/meson.build Normal file
View File

@ -0,0 +1,76 @@
# SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
# 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',
)

214
nss/nss_adapters.cc Normal file
View File

@ -0,0 +1,214 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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 <cerrno>
#include <new>
#include <nss.h>
#include <sys/socket.h>
#include <exception>
#include <memory>
#include <string>
#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<char[]>(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
);
}

304
nss/resolver.cc Normal file
View File

@ -0,0 +1,304 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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 <ares.h>
#include <arpa/nameser.h>
#include <cstddef>
#include <new>
#include <nss.h>
#include <sys/epoll.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <cerrno>
#include <cstring>
#include <format>
#include <regex>
#include <system_error>
#include <stdexcept>
using namespace std::literals;
namespace /* anonymous */ {
constexpr const auto CANARY_HOSTNAME = "use-application-dns.net"sv;
auto init_channel(const std::vector<std::string> dns) -> ares_channel_t *;
auto setup_servers(ares_channel_t *channel, const std::vector<std::string> 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<ares_addr_port_node, CAresAddrListDeletor>;
struct CallbackArgs {
malcontent::Resolver *resolver;
malcontent::ResolverArgs *args;
nss_status return_status = NSS_STATUS_TRYAGAIN;
};
} // ~ anonymous namespace
malcontent::Resolver::Resolver(std::vector<std::string> 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<CallbackArgs *>(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<std::string> 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<std::string> dns) -> void {
CAresAddrList list;
for (auto it = dns.crbegin(); it != dns.crend(); ++it) {
auto new_node = std::make_unique<ares_addr_port_node>();
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 '<ip>[: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

61
nss/resolver.hh Normal file
View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "wrapper.hh"
#include "cares_init.hh"
#include <ares.h>
#include <nss.h>
#include <memory>
#include <string>
#include <vector>
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<std::string> 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<ares_channel_t, CAresDeletor>;
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<CAresLibrary> _ensure_cares_initialized;
CAresChannel _channel;
};
} // ~ namespace malcontent

21
nss/test_main.cc Normal file
View File

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "testsuite.hh"
#include <glib.h>
#include <clocale>
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();
}

192
nss/test_resolver.cc Normal file
View File

@ -0,0 +1,192 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "testsuite.hh"
#include "resolver.hh"
#include <glib.h>
#include <array>
#include <format>
#include <netdb.h>
#include <sys/socket.h>
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<const char *>(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<const char *>(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<const char *>(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<const char *>(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

10
nss/testsuite.hh Normal file
View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <glib.h>
#define g_assert_unreachable(msg) g_assert_cmpstr(msg, ==, nullptr)
auto define_resolver_tests() -> void;

78
nss/user_policy.cc Normal file
View File

@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "user_policy.hh"
#include "logger.hh"
#include "resolver.hh"
#include "parentalcontrol-dns.h"
#include <mutex>
#include <string>
#include <vector>
#include <unistd.h>
namespace /* anonymous */ {
struct GObjectDeletor {
auto operator() (void *gobj) const noexcept -> void {
g_object_unref(gobj);
}
};
} // ~ namespace anonymous
using ProxyPtr = std::unique_ptr<ParentalControlsDns, GObjectDeletor>;
auto malcontent::UserPolicy::resolver(std::optional<uid_t> maybe_uid) const -> std::shared_ptr<Resolver> {
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<std::string> 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<Resolver>(std::move(dns)) });
return it->second;
}

31
nss/user_policy.hh Normal file
View File

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <shared_mutex>
#include <optional>
#include <unordered_map>
#include <unistd.h>
namespace malcontent {
class Resolver;
class UserPolicy {
public:
UserPolicy() = default;
UserPolicy(const UserPolicy&) = delete;
UserPolicy(UserPolicy&&) = delete;
~UserPolicy() noexcept = default;
auto resolver(std::optional<uid_t> uid = {}) const -> std::shared_ptr<Resolver>;
private:
mutable std::unordered_map<uid_t, std::shared_ptr<Resolver>> _resolver_cache;
mutable std::shared_mutex _cache_mutex;
};
} // ~ namespace malcontent

43
nss/wrapper.hh Normal file
View File

@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <nss.h>
#include <netdb.h>
// 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 */
};