WIP: add DNS parental controls (C++ version) #1
11
meson.build
11
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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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