dns: c++ implementation of nss module
This commit is contained in:
parent
3beee1cf84
commit
596b5f7f5a
11
meson.build
11
meson.build
|
@ -1,4 +1,4 @@
|
||||||
project('malcontent', 'c',
|
project('malcontent', 'c', 'cpp',
|
||||||
version : '0.12.0',
|
version : '0.12.0',
|
||||||
meson_version : '>= 0.59.0',
|
meson_version : '>= 0.59.0',
|
||||||
license: ['LGPL-2.1-or-later', 'GPL-2.0-or-later'],
|
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')
|
gnome = import('gnome')
|
||||||
i18n = import('i18n')
|
i18n = import('i18n')
|
||||||
pkgconfig = import('pkgconfig')
|
pkgconfig = import('pkgconfig')
|
||||||
|
@ -152,5 +156,10 @@ if get_option('ui').enabled()
|
||||||
update_desktop_database: true,
|
update_desktop_database: true,
|
||||||
)
|
)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
if get_option('nss').enabled()
|
||||||
|
subdir('nss')
|
||||||
|
endif
|
||||||
|
|
||||||
subdir('pam')
|
subdir('pam')
|
||||||
subdir('po')
|
subdir('po')
|
||||||
|
|
|
@ -9,6 +9,12 @@ option(
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'directory for PAM modules'
|
description: 'directory for PAM modules'
|
||||||
)
|
)
|
||||||
|
option(
|
||||||
|
'nss',
|
||||||
|
type: 'feature',
|
||||||
|
value: 'enabled',
|
||||||
|
description: 'enable NSS module support (for parental DNS controls)'
|
||||||
|
)
|
||||||
option(
|
option(
|
||||||
'ui',
|
'ui',
|
||||||
type: 'feature',
|
type: 'feature',
|
||||||
|
|
|
@ -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{};
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
|
@ -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',
|
||||||
|
)
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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 */
|
||||||
|
};
|
Loading…
Reference in New Issue