305 lines
9.8 KiB
C++
305 lines
9.8 KiB
C++
|
// 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
|