2021-03-09 22:24:35 +00:00
|
|
|
#include "server.hpp"
|
2023-10-19 21:59:57 +00:00
|
|
|
|
2023-10-24 13:18:03 +00:00
|
|
|
#include "nm_platform.hpp"
|
|
|
|
#include "sd_platform.hpp"
|
|
|
|
|
2022-09-13 20:19:28 +00:00
|
|
|
#include <llarp/constants/apple.hpp>
|
2023-10-24 13:18:03 +00:00
|
|
|
#include <llarp/constants/platform.hpp>
|
2023-10-19 21:59:57 +00:00
|
|
|
#include <llarp/ev/udp_handle.hpp>
|
2023-10-24 13:18:03 +00:00
|
|
|
|
|
|
|
#include <oxen/log.hpp>
|
2023-10-19 21:59:57 +00:00
|
|
|
#include <unbound.h>
|
2023-10-24 13:18:03 +00:00
|
|
|
#include <uvw.hpp>
|
|
|
|
|
|
|
|
#include <memory>
|
|
|
|
#include <optional>
|
2022-09-12 16:05:49 +00:00
|
|
|
#include <stdexcept>
|
2019-07-30 23:42:13 +00:00
|
|
|
#include <utility>
|
2023-10-19 21:59:57 +00:00
|
|
|
|
2021-03-02 18:18:22 +00:00
|
|
|
namespace llarp::dns
|
2018-12-03 22:22:59 +00:00
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
static auto logcat = log::Cat("dns");
|
|
|
|
|
2021-03-02 18:18:22 +00:00
|
|
|
void
|
2022-10-31 20:06:11 +00:00
|
|
|
QueryJob_Base::Cancel()
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
Message reply{m_Query};
|
|
|
|
reply.AddServFail();
|
|
|
|
SendReply(reply.ToBuffer());
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2018-12-03 22:22:59 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
/// sucks up udp packets from a bound socket and feeds it to a server
|
|
|
|
class UDPReader : public PacketSource_Base, public std::enable_shared_from_this<UDPReader>
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
Server& m_DNS;
|
|
|
|
std::shared_ptr<llarp::UDPHandle> m_udp;
|
|
|
|
SockAddr m_LocalAddr;
|
2018-12-04 16:35:25 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
public:
|
|
|
|
explicit UDPReader(Server& dns, const EventLoop_ptr& loop, llarp::SockAddr bindaddr)
|
|
|
|
: m_DNS{dns}
|
2018-12-04 16:35:25 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
m_udp = loop->make_udp([&](auto&, SockAddr src, llarp::OwnedBuffer buf) {
|
|
|
|
if (src == m_LocalAddr)
|
|
|
|
return;
|
2022-07-28 16:07:38 +00:00
|
|
|
if (not m_DNS.MaybeHandlePacket(shared_from_this(), m_LocalAddr, src, std::move(buf)))
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
log::warning(logcat, "did not handle dns packet from {} to {}", src, m_LocalAddr);
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
m_udp->listen(bindaddr);
|
|
|
|
if (auto maybe_addr = BoundOn())
|
|
|
|
{
|
|
|
|
m_LocalAddr = *maybe_addr;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
throw std::runtime_error{"cannot find which address our dns socket is bound on"};
|
2018-12-03 22:22:59 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
std::optional<SockAddr>
|
|
|
|
BoundOn() const override
|
|
|
|
{
|
|
|
|
return m_udp->LocalAddr();
|
|
|
|
}
|
2018-12-03 22:22:59 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
bool
|
|
|
|
WouldLoop(const SockAddr& to, const SockAddr&) const override
|
|
|
|
{
|
|
|
|
return to != m_LocalAddr;
|
|
|
|
}
|
2020-06-09 16:59:49 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
void
|
|
|
|
SendTo(const SockAddr& to, const SockAddr&, llarp::OwnedBuffer buf) const override
|
|
|
|
{
|
|
|
|
m_udp->send(to, std::move(buf));
|
|
|
|
}
|
2020-06-15 18:15:10 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
void
|
|
|
|
Stop() override
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
m_udp->close();
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
namespace libunbound
|
|
|
|
{
|
|
|
|
class Resolver;
|
|
|
|
|
2022-10-31 17:15:29 +00:00
|
|
|
class Query : public QueryJob_Base, public std::enable_shared_from_this<Query>
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-07-28 16:07:38 +00:00
|
|
|
std::shared_ptr<PacketSource_Base> src;
|
2022-04-07 20:44:23 +00:00
|
|
|
SockAddr resolverAddr;
|
|
|
|
SockAddr askerAddr;
|
|
|
|
|
|
|
|
public:
|
|
|
|
explicit Query(
|
|
|
|
std::weak_ptr<Resolver> parent_,
|
|
|
|
Message query,
|
2022-07-28 16:07:38 +00:00
|
|
|
std::shared_ptr<PacketSource_Base> pktsrc,
|
2022-04-07 20:44:23 +00:00
|
|
|
SockAddr toaddr,
|
|
|
|
SockAddr fromaddr)
|
|
|
|
: QueryJob_Base{std::move(query)}
|
2022-09-12 22:11:25 +00:00
|
|
|
, src{std::move(pktsrc)}
|
2022-04-07 20:44:23 +00:00
|
|
|
, resolverAddr{std::move(toaddr)}
|
|
|
|
, askerAddr{std::move(fromaddr)}
|
2022-10-17 13:10:34 +00:00
|
|
|
, parent{parent_}
|
2022-04-07 20:44:23 +00:00
|
|
|
{}
|
2022-10-17 13:10:34 +00:00
|
|
|
std::weak_ptr<Resolver> parent;
|
|
|
|
int id{};
|
2022-04-07 20:44:23 +00:00
|
|
|
|
2022-10-31 20:06:11 +00:00
|
|
|
void
|
|
|
|
SendReply(llarp::OwnedBuffer replyBuf) override;
|
2022-04-07 20:44:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/// Resolver_Base that uses libunbound
|
2022-09-16 23:55:59 +00:00
|
|
|
class Resolver final : public Resolver_Base, public std::enable_shared_from_this<Resolver>
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
ub_ctx* m_ctx = nullptr;
|
2022-04-07 20:44:23 +00:00
|
|
|
std::weak_ptr<EventLoop> m_Loop;
|
|
|
|
#ifdef _WIN32
|
|
|
|
// windows is dumb so we do ub mainloop in a thread
|
|
|
|
std::thread runner;
|
|
|
|
std::atomic<bool> running;
|
|
|
|
#else
|
|
|
|
std::shared_ptr<uvw::PollHandle> m_Poller;
|
|
|
|
#endif
|
|
|
|
|
2022-07-28 16:07:38 +00:00
|
|
|
std::optional<SockAddr> m_LocalAddr;
|
2022-10-31 20:06:11 +00:00
|
|
|
std::unordered_set<std::shared_ptr<Query>> m_Pending;
|
2022-10-17 13:10:34 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
struct ub_result_deleter
|
|
|
|
{
|
|
|
|
void
|
|
|
|
operator()(ub_result* ptr)
|
|
|
|
{
|
|
|
|
::ub_resolve_free(ptr);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2022-07-28 16:07:38 +00:00
|
|
|
const net::Platform*
|
|
|
|
Net_ptr() const
|
|
|
|
{
|
|
|
|
return m_Loop.lock()->Net_ptr();
|
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
static void
|
|
|
|
Callback(void* data, int err, ub_result* _result)
|
|
|
|
{
|
2022-11-22 21:27:21 +00:00
|
|
|
log::debug(logcat, "got dns response from libunbound");
|
2022-04-07 20:44:23 +00:00
|
|
|
// take ownership of ub_result
|
|
|
|
std::unique_ptr<ub_result, ub_result_deleter> result{_result};
|
2022-10-31 17:15:29 +00:00
|
|
|
// borrow query
|
2022-10-31 20:06:11 +00:00
|
|
|
auto* query = static_cast<Query*>(data);
|
2022-04-07 20:44:23 +00:00
|
|
|
if (err)
|
|
|
|
{
|
|
|
|
// some kind of error from upstream
|
2022-09-13 20:39:12 +00:00
|
|
|
log::warning(logcat, "Upstream DNS failure: {}", ub_strerror(err));
|
2022-04-07 20:44:23 +00:00
|
|
|
query->Cancel();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-11-22 21:27:21 +00:00
|
|
|
log::trace(logcat, "queueing dns response from libunbound to userland");
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
// rewrite response
|
|
|
|
OwnedBuffer pkt{(const byte_t*)result->answer_packet, (size_t)result->answer_len};
|
|
|
|
llarp_buffer_t buf{pkt};
|
|
|
|
MessageHeader hdr;
|
|
|
|
hdr.Decode(&buf);
|
|
|
|
hdr.id = query->Underlying().hdr_id;
|
|
|
|
buf.cur = buf.base;
|
|
|
|
hdr.Encode(&buf);
|
2022-10-31 17:15:29 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
// send reply
|
|
|
|
query->SendReply(std::move(pkt));
|
|
|
|
}
|
|
|
|
|
2022-09-16 16:20:24 +00:00
|
|
|
void
|
|
|
|
AddUpstreamResolver(const SockAddr& dns)
|
2022-07-28 16:07:38 +00:00
|
|
|
{
|
2023-05-20 22:18:40 +00:00
|
|
|
std::string str = fmt::format("{}@{}", dns.hostString(false), dns.getPort());
|
2022-09-16 16:20:24 +00:00
|
|
|
|
2022-09-16 23:55:59 +00:00
|
|
|
if (auto err = ub_ctx_set_fwd(m_ctx, str.c_str()))
|
2022-09-16 16:20:24 +00:00
|
|
|
{
|
|
|
|
throw std::runtime_error{
|
|
|
|
fmt::format("cannot use {} as upstream dns: {}", str, ub_strerror(err))};
|
|
|
|
}
|
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
|
2022-09-16 20:41:52 +00:00
|
|
|
bool
|
|
|
|
ConfigureAppleTrampoline(const SockAddr& dns)
|
2022-09-16 16:20:24 +00:00
|
|
|
{
|
2022-09-16 20:41:52 +00:00
|
|
|
// On Apple, when we turn on exit mode, we tear down and then reestablish the unbound
|
|
|
|
// resolver: in exit mode, we set use upstream to a localhost trampoline that redirects
|
|
|
|
// packets through the tunnel. In non-exit mode, we directly use the upstream, so we look
|
|
|
|
// here for a reconfiguration to use the trampoline port to check which state we're in.
|
|
|
|
//
|
|
|
|
// We have to do all this crap because we can't directly connect to upstream from here:
|
|
|
|
// within the network extension, macOS ignores the tunnel we are managing and so, if we
|
|
|
|
// didn't do this, all our DNS queries would leak out around the tunnel. Instead we have to
|
|
|
|
// bounce things through the objective C trampoline code (which is what actually handles the
|
|
|
|
// upstream querying) so that it can call into Apple's special snowflake API to set up a
|
|
|
|
// socket that has the magic Apple snowflake sauce added on top so that it actually routes
|
|
|
|
// through the tunnel instead of around it.
|
|
|
|
//
|
|
|
|
// But the trampoline *always* tries to send the packet through the tunnel, and that will
|
|
|
|
// only work in exit mode.
|
|
|
|
//
|
|
|
|
// All of this macos behaviour is all carefully and explicitly documented by Apple with
|
|
|
|
// plenty of examples and other exposition, of course, just like all of their wonderful new
|
|
|
|
// APIs to reinvent standard unix interfaces with half-baked replacements.
|
|
|
|
|
2022-09-16 15:23:25 +00:00
|
|
|
if constexpr (platform::is_apple)
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-09-16 20:41:52 +00:00
|
|
|
if (dns.hostString() == "127.0.0.1" and dns.getPort() == apple::dns_trampoline_port)
|
|
|
|
{
|
2022-09-19 15:22:28 +00:00
|
|
|
// macOS is stupid: the default (0.0.0.0) fails with "send failed: Can't assign
|
|
|
|
// requested address" when unbound tries to connect to the localhost address using a
|
|
|
|
// source address of 0.0.0.0. Yay apple.
|
2022-09-16 20:41:52 +00:00
|
|
|
SetOpt("outgoing-interface:", "127.0.0.1");
|
|
|
|
|
|
|
|
// The trampoline expects just a single source port (and sends everything back to it).
|
|
|
|
SetOpt("outgoing-range:", "1");
|
|
|
|
SetOpt("outgoing-port-avoid:", "0-65535");
|
|
|
|
SetOpt("outgoing-port-permit:", "{}", apple::dns_trampoline_source_port);
|
|
|
|
return true;
|
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
2022-09-16 20:41:52 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ConfigureUpstream(const llarp::DnsConfig& conf)
|
|
|
|
{
|
|
|
|
bool is_apple_tramp = false;
|
2022-04-07 20:44:23 +00:00
|
|
|
|
|
|
|
// set up forward dns
|
2023-11-03 13:40:14 +00:00
|
|
|
for (const auto& dns : conf.upstream_dns)
|
2022-09-16 20:41:52 +00:00
|
|
|
{
|
2022-09-16 16:20:24 +00:00
|
|
|
AddUpstreamResolver(dns);
|
2022-09-16 20:41:52 +00:00
|
|
|
is_apple_tramp = is_apple_tramp or ConfigureAppleTrampoline(dns);
|
|
|
|
}
|
2022-07-28 16:07:38 +00:00
|
|
|
|
2023-11-03 13:40:14 +00:00
|
|
|
if (auto maybe_addr = conf.query_bind; maybe_addr and not is_apple_tramp)
|
2022-07-28 16:07:38 +00:00
|
|
|
{
|
|
|
|
SockAddr addr{*maybe_addr};
|
|
|
|
std::string host{addr.hostString()};
|
|
|
|
|
|
|
|
if (addr.getPort() == 0)
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-07-28 16:07:38 +00:00
|
|
|
// unbound manages their own sockets because of COURSE it does. so we find an open port
|
|
|
|
// on our system and use it so we KNOW what it is before giving it to unbound to
|
|
|
|
// explicitly bind to JUST that port.
|
|
|
|
|
2022-09-13 20:24:25 +00:00
|
|
|
auto fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
|
|
#ifdef _WIN32
|
|
|
|
if (fd == INVALID_SOCKET)
|
|
|
|
#else
|
2022-09-12 16:05:49 +00:00
|
|
|
if (fd == -1)
|
2022-09-13 20:24:25 +00:00
|
|
|
#endif
|
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
throw std::invalid_argument{
|
|
|
|
fmt::format("Failed to create UDP socket for unbound: {}", strerror(errno))};
|
2022-09-13 20:24:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
#define CLOSE closesocket
|
|
|
|
#else
|
|
|
|
#define CLOSE close
|
|
|
|
#endif
|
2022-09-12 16:17:22 +00:00
|
|
|
if (0 != bind(fd, static_cast<const sockaddr*>(addr), addr.sockaddr_len()))
|
|
|
|
{
|
2022-09-13 20:24:25 +00:00
|
|
|
CLOSE(fd);
|
2022-09-12 16:17:22 +00:00
|
|
|
throw std::invalid_argument{
|
|
|
|
fmt::format("Failed to bind UDP socket for unbound: {}", strerror(errno))};
|
2022-09-12 16:05:49 +00:00
|
|
|
}
|
|
|
|
struct sockaddr_storage sas;
|
|
|
|
auto* sa = reinterpret_cast<struct sockaddr*>(&sas);
|
2022-09-12 20:22:40 +00:00
|
|
|
socklen_t sa_len = sizeof(sas);
|
2022-09-13 20:24:25 +00:00
|
|
|
int rc = getsockname(fd, sa, &sa_len);
|
|
|
|
CLOSE(fd);
|
|
|
|
#undef CLOSE
|
|
|
|
if (rc != 0)
|
2022-09-12 16:17:22 +00:00
|
|
|
{
|
|
|
|
throw std::invalid_argument{
|
|
|
|
fmt::format("Failed to query UDP port for unbound: {}", strerror(errno))};
|
2022-09-12 16:05:49 +00:00
|
|
|
}
|
|
|
|
addr = SockAddr{*sa};
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
2022-07-28 16:07:38 +00:00
|
|
|
m_LocalAddr = addr;
|
|
|
|
|
2022-09-12 16:17:22 +00:00
|
|
|
log::info(logcat, "sending dns queries from {}:{}", host, addr.getPort());
|
2022-07-28 16:07:38 +00:00
|
|
|
// set up query bind port if needed
|
|
|
|
SetOpt("outgoing-interface:", host);
|
|
|
|
SetOpt("outgoing-range:", "1");
|
|
|
|
SetOpt("outgoing-port-avoid:", "0-65535");
|
2022-09-16 16:20:24 +00:00
|
|
|
SetOpt("outgoing-port-permit:", "{}", addr.getPort());
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
2022-09-16 15:23:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2022-09-16 16:20:24 +00:00
|
|
|
SetOpt(const std::string& key, const std::string& val)
|
2022-09-16 15:23:25 +00:00
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
ub_ctx_set_option(m_ctx, key.c_str(), val.c_str());
|
2022-09-16 15:23:25 +00:00
|
|
|
}
|
|
|
|
|
2022-09-16 16:20:24 +00:00
|
|
|
// Wrapper around the above that takes 3+ arguments: the 2nd arg gets formatted with the
|
|
|
|
// remaining args, and the formatted string passed to the above as `val`.
|
|
|
|
template <typename... FmtArgs, std::enable_if_t<sizeof...(FmtArgs), int> = 0>
|
|
|
|
void
|
|
|
|
SetOpt(const std::string& key, std::string_view format, FmtArgs&&... args)
|
|
|
|
{
|
|
|
|
SetOpt(key, fmt::format(format, std::forward<FmtArgs>(args)...));
|
|
|
|
}
|
|
|
|
|
2022-09-16 23:55:59 +00:00
|
|
|
// Copy of the DNS config (a copy because on some platforms, like Apple, we change the applied
|
|
|
|
// upstream DNS settings when turning on/off exit mode).
|
2022-09-16 15:23:25 +00:00
|
|
|
llarp::DnsConfig m_conf;
|
|
|
|
|
|
|
|
public:
|
|
|
|
explicit Resolver(const EventLoop_ptr& loop, llarp::DnsConfig conf)
|
2022-09-16 23:55:59 +00:00
|
|
|
: m_Loop{loop}, m_conf{std::move(conf)}
|
2022-09-16 15:23:25 +00:00
|
|
|
{
|
|
|
|
Up(m_conf);
|
|
|
|
}
|
|
|
|
|
2022-09-16 23:55:59 +00:00
|
|
|
~Resolver() override
|
2022-09-16 15:23:25 +00:00
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
Down();
|
2022-09-16 15:23:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string_view
|
|
|
|
ResolverName() const override
|
|
|
|
{
|
|
|
|
return "unbound";
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual std::optional<SockAddr>
|
|
|
|
GetLocalAddr() const override
|
|
|
|
{
|
|
|
|
return m_LocalAddr;
|
|
|
|
}
|
|
|
|
|
2022-10-31 17:15:29 +00:00
|
|
|
void
|
2022-10-31 20:06:11 +00:00
|
|
|
RemovePending(const std::shared_ptr<Query>& query)
|
2022-10-31 17:15:29 +00:00
|
|
|
{
|
2022-10-31 20:06:11 +00:00
|
|
|
m_Pending.erase(query);
|
2022-10-31 17:15:29 +00:00
|
|
|
}
|
|
|
|
|
2022-09-16 15:23:25 +00:00
|
|
|
void
|
|
|
|
Up(const llarp::DnsConfig& conf)
|
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
if (m_ctx)
|
|
|
|
throw std::logic_error{"Internal error: attempt to Up() dns server multiple times"};
|
|
|
|
|
|
|
|
m_ctx = ::ub_ctx_create();
|
2022-09-16 15:23:25 +00:00
|
|
|
// set libunbound settings
|
|
|
|
|
|
|
|
SetOpt("do-tcp:", "no");
|
|
|
|
|
2023-11-03 13:40:14 +00:00
|
|
|
for (const auto& [k, v] : conf.extra_opts)
|
2022-09-16 15:23:25 +00:00
|
|
|
SetOpt(k, v);
|
|
|
|
|
|
|
|
// add host files
|
2023-11-03 13:40:14 +00:00
|
|
|
for (const auto& file : conf.hostfiles)
|
2022-09-16 15:23:25 +00:00
|
|
|
{
|
|
|
|
const auto str = file.u8string();
|
2022-09-16 23:55:59 +00:00
|
|
|
if (auto ret = ub_ctx_hosts(m_ctx, str.c_str()))
|
2022-09-16 15:23:25 +00:00
|
|
|
{
|
|
|
|
throw std::runtime_error{
|
|
|
|
fmt::format("Failed to add host file {}: {}", file, ub_strerror(ret))};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ConfigureUpstream(conf);
|
2022-07-28 16:07:38 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
// set async
|
2022-09-16 23:55:59 +00:00
|
|
|
ub_ctx_async(m_ctx, 1);
|
2022-04-07 20:44:23 +00:00
|
|
|
// setup mainloop
|
|
|
|
#ifdef _WIN32
|
|
|
|
running = true;
|
2022-09-16 23:55:59 +00:00
|
|
|
runner = std::thread{[this]() {
|
2022-04-07 20:44:23 +00:00
|
|
|
while (running)
|
|
|
|
{
|
2022-10-31 17:15:29 +00:00
|
|
|
// poll and process callbacks it this thread
|
|
|
|
if (ub_poll(m_ctx))
|
|
|
|
{
|
|
|
|
ub_process(m_ctx);
|
|
|
|
}
|
|
|
|
else // nothing to do, sleep.
|
|
|
|
std::this_thread::sleep_for(10ms);
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
}};
|
|
|
|
#else
|
|
|
|
if (auto loop = m_Loop.lock())
|
|
|
|
{
|
|
|
|
if (auto loop_ptr = loop->MaybeGetUVWLoop())
|
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
m_Poller = loop_ptr->resource<uvw::PollHandle>(ub_fd(m_ctx));
|
2022-09-19 15:22:28 +00:00
|
|
|
m_Poller->on<uvw::PollEvent>([this](auto&, auto&) { ub_process(m_ctx); });
|
2022-04-07 20:44:23 +00:00
|
|
|
m_Poller->start(uvw::PollHandle::Event::READABLE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw std::runtime_error{"no uvw loop"};
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2022-09-16 23:55:59 +00:00
|
|
|
Down() override
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
|
|
|
#ifdef _WIN32
|
2022-09-16 23:55:59 +00:00
|
|
|
if (running.exchange(false))
|
2022-10-31 17:15:29 +00:00
|
|
|
{
|
|
|
|
log::debug(logcat, "shutting down win32 dns thread");
|
2022-09-16 23:55:59 +00:00
|
|
|
runner.join();
|
2022-10-31 17:15:29 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
#else
|
2022-09-16 23:55:59 +00:00
|
|
|
if (m_Poller)
|
|
|
|
m_Poller->close();
|
2022-04-07 20:44:23 +00:00
|
|
|
#endif
|
2022-09-19 15:22:28 +00:00
|
|
|
if (m_ctx)
|
|
|
|
{
|
2022-09-16 23:55:59 +00:00
|
|
|
::ub_ctx_delete(m_ctx);
|
|
|
|
m_ctx = nullptr;
|
2022-10-31 20:06:11 +00:00
|
|
|
|
|
|
|
// destroy any outstanding queries that unbound hasn't fired yet
|
|
|
|
if (not m_Pending.empty())
|
|
|
|
{
|
|
|
|
log::debug(logcat, "cancelling {} pending queries", m_Pending.size());
|
2022-11-01 00:22:13 +00:00
|
|
|
// We must copy because Cancel does a loop call to remove itself, but since we are
|
|
|
|
// already in the main loop it happens immediately, which would invalidate our iterator
|
|
|
|
// if we were looping through m_Pending at the time.
|
|
|
|
auto copy = m_Pending;
|
|
|
|
for (const auto& query : copy)
|
2022-10-31 20:06:11 +00:00
|
|
|
query->Cancel();
|
|
|
|
}
|
2022-09-16 23:55:59 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
Rank() const override
|
|
|
|
{
|
|
|
|
return 10;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2022-09-16 23:55:59 +00:00
|
|
|
ResetResolver(std::optional<std::vector<SockAddr>> replace_upstream) override
|
2020-06-09 16:59:49 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
Down();
|
2022-09-16 19:27:12 +00:00
|
|
|
if (replace_upstream)
|
2023-11-03 13:40:14 +00:00
|
|
|
m_conf.upstream_dns = std::move(*replace_upstream);
|
2022-04-07 20:44:23 +00:00
|
|
|
Up(m_conf);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename Callable>
|
|
|
|
void
|
|
|
|
call(Callable&& f)
|
|
|
|
{
|
|
|
|
if (auto loop = m_Loop.lock())
|
|
|
|
loop->call(std::forward<Callable>(f));
|
|
|
|
else
|
2022-09-12 16:17:22 +00:00
|
|
|
log::critical(logcat, "no mainloop?");
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
MaybeHookDNS(
|
2022-07-28 16:07:38 +00:00
|
|
|
std::shared_ptr<PacketSource_Base> source,
|
2022-04-07 20:44:23 +00:00
|
|
|
const Message& query,
|
|
|
|
const SockAddr& to,
|
|
|
|
const SockAddr& from) override
|
|
|
|
{
|
2022-10-31 17:15:29 +00:00
|
|
|
auto tmp = std::make_shared<Query>(weak_from_this(), query, source, to, from);
|
2022-04-07 20:44:23 +00:00
|
|
|
// no questions, send fail
|
|
|
|
if (query.questions.empty())
|
|
|
|
{
|
2022-11-22 21:27:21 +00:00
|
|
|
log::info(
|
|
|
|
logcat,
|
|
|
|
"dns from {} to {} has empty query questions, sending failure reply",
|
|
|
|
from,
|
|
|
|
to);
|
2022-04-07 20:44:23 +00:00
|
|
|
tmp->Cancel();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const auto& q : query.questions)
|
|
|
|
{
|
|
|
|
// dont process .loki or .snode
|
|
|
|
if (q.HasTLD(".loki") or q.HasTLD(".snode"))
|
|
|
|
{
|
2022-11-22 21:27:21 +00:00
|
|
|
log::warning(
|
|
|
|
logcat,
|
|
|
|
"dns from {} to {} is for .loki or .snode but got to the unbound resolver, sending "
|
|
|
|
"failure reply",
|
|
|
|
from,
|
|
|
|
to);
|
2022-04-07 20:44:23 +00:00
|
|
|
tmp->Cancel();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
2022-10-17 23:05:30 +00:00
|
|
|
if (not m_ctx)
|
2022-10-17 13:38:19 +00:00
|
|
|
{
|
|
|
|
// we are down
|
2022-11-22 21:27:21 +00:00
|
|
|
log::debug(
|
|
|
|
logcat,
|
|
|
|
"dns from {} to {} got to the unbound resolver, but the resolver isn't set up, "
|
|
|
|
"sending failure reply",
|
|
|
|
from,
|
|
|
|
to);
|
2022-10-17 13:38:19 +00:00
|
|
|
tmp->Cancel();
|
|
|
|
return true;
|
|
|
|
}
|
2022-10-31 17:15:29 +00:00
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
if (not running)
|
|
|
|
{
|
|
|
|
// we are stopping the win32 thread
|
2022-11-22 21:27:21 +00:00
|
|
|
log::debug(
|
|
|
|
logcat,
|
|
|
|
"dns from {} to {} got to the unbound resolver, but the resolver isn't running, "
|
|
|
|
"sending failure reply",
|
|
|
|
from,
|
|
|
|
to);
|
2022-10-31 17:15:29 +00:00
|
|
|
tmp->Cancel();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
#endif
|
2022-04-07 20:44:23 +00:00
|
|
|
const auto& q = query.questions[0];
|
|
|
|
if (auto err = ub_resolve_async(
|
2022-09-16 23:55:59 +00:00
|
|
|
m_ctx,
|
2022-04-07 20:44:23 +00:00
|
|
|
q.Name().c_str(),
|
|
|
|
q.qtype,
|
|
|
|
q.qclass,
|
2022-09-12 22:11:25 +00:00
|
|
|
tmp.get(),
|
2022-04-07 20:44:23 +00:00
|
|
|
&Resolver::Callback,
|
2022-10-31 20:06:11 +00:00
|
|
|
nullptr))
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
log::warning(
|
|
|
|
logcat, "failed to send upstream query with libunbound: {}", ub_strerror(err));
|
2022-04-07 20:44:23 +00:00
|
|
|
tmp->Cancel();
|
2022-09-13 20:39:12 +00:00
|
|
|
}
|
|
|
|
else
|
2022-11-22 21:27:21 +00:00
|
|
|
{
|
|
|
|
log::trace(logcat, "dns from {} to {} processing via libunbound", from, to);
|
2022-10-31 20:06:11 +00:00
|
|
|
m_Pending.insert(std::move(tmp));
|
2022-11-22 21:27:21 +00:00
|
|
|
}
|
2022-10-31 17:15:29 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
return true;
|
2020-06-09 16:59:49 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
void
|
2022-10-31 20:06:11 +00:00
|
|
|
Query::SendReply(llarp::OwnedBuffer replyBuf)
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-10-31 20:06:11 +00:00
|
|
|
if (m_Done.test_and_set())
|
|
|
|
return;
|
2022-10-31 17:15:29 +00:00
|
|
|
auto parent_ptr = parent.lock();
|
|
|
|
if (parent_ptr)
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-10-31 20:06:11 +00:00
|
|
|
parent_ptr->call(
|
|
|
|
[self = shared_from_this(), parent_ptr = std::move(parent_ptr), buf = replyBuf.copy()] {
|
2022-11-22 21:27:21 +00:00
|
|
|
log::trace(
|
|
|
|
logcat,
|
|
|
|
"forwarding dns response from libunbound to userland (resolverAddr: {}, "
|
|
|
|
"askerAddr: {})",
|
|
|
|
self->resolverAddr,
|
|
|
|
self->askerAddr);
|
2022-10-31 20:06:11 +00:00
|
|
|
self->src->SendTo(self->askerAddr, self->resolverAddr, OwnedBuffer::copy_from(buf));
|
|
|
|
// remove query
|
|
|
|
parent_ptr->RemovePending(self);
|
|
|
|
});
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
else
|
2022-10-31 17:15:29 +00:00
|
|
|
log::error(logcat, "no parent");
|
2020-06-09 16:59:49 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
} // namespace libunbound
|
|
|
|
|
2022-07-28 16:07:38 +00:00
|
|
|
Server::Server(EventLoop_ptr loop, llarp::DnsConfig conf, unsigned int netif)
|
2022-06-22 16:14:33 +00:00
|
|
|
: m_Loop{std::move(loop)}
|
|
|
|
, m_Config{std::move(conf)}
|
|
|
|
, m_Platform{CreatePlatform()}
|
2022-07-28 16:07:38 +00:00
|
|
|
, m_NetIfIndex{std::move(netif)}
|
2022-04-07 20:44:23 +00:00
|
|
|
{}
|
|
|
|
|
2022-07-28 16:07:38 +00:00
|
|
|
std::vector<std::weak_ptr<Resolver_Base>>
|
|
|
|
Server::GetAllResolvers() const
|
|
|
|
{
|
2022-09-13 20:39:12 +00:00
|
|
|
return {m_Resolvers.begin(), m_Resolvers.end()};
|
2022-07-28 16:07:38 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
void
|
|
|
|
Server::Start()
|
|
|
|
{
|
|
|
|
// set up udp sockets
|
2023-11-03 13:40:14 +00:00
|
|
|
for (const auto& addr : m_Config.bind_addr)
|
2021-06-09 10:38:11 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto ptr = MakePacketSourceOn(addr, m_Config))
|
|
|
|
AddPacketSource(std::move(ptr));
|
2021-06-09 10:38:11 +00:00
|
|
|
}
|
2020-06-09 16:59:49 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
// add default resolver as needed
|
|
|
|
if (auto ptr = MakeDefaultResolver())
|
|
|
|
AddResolver(ptr);
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2018-12-03 22:22:59 +00:00
|
|
|
|
2022-06-22 16:14:33 +00:00
|
|
|
std::shared_ptr<I_Platform>
|
|
|
|
Server::CreatePlatform() const
|
|
|
|
{
|
|
|
|
auto plat = std::make_shared<Multi_Platform>();
|
|
|
|
if constexpr (llarp::platform::has_systemd)
|
|
|
|
{
|
|
|
|
plat->add_impl(std::make_unique<SD_Platform_t>());
|
|
|
|
plat->add_impl(std::make_unique<NM_Platform_t>());
|
|
|
|
}
|
|
|
|
return plat;
|
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
std::shared_ptr<PacketSource_Base>
|
|
|
|
Server::MakePacketSourceOn(const llarp::SockAddr& addr, const llarp::DnsConfig&)
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
return std::make_shared<UDPReader>(*this, m_Loop, addr);
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
std::shared_ptr<Resolver_Base>
|
|
|
|
Server::MakeDefaultResolver()
|
2021-09-01 18:31:45 +00:00
|
|
|
{
|
2023-11-03 13:40:14 +00:00
|
|
|
if (m_Config.upstream_dns.empty())
|
2022-04-07 20:44:23 +00:00
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
log::info(
|
|
|
|
logcat,
|
2022-04-07 20:44:23 +00:00
|
|
|
"explicitly no upstream dns providers specified, we will not resolve anything but .loki "
|
|
|
|
"and .snode");
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return std::make_shared<libunbound::Resolver>(m_Loop, m_Config);
|
2021-09-01 18:10:08 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
std::vector<SockAddr>
|
|
|
|
Server::BoundPacketSourceAddrs() const
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
std::vector<SockAddr> addrs;
|
|
|
|
for (const auto& src : m_PacketSources)
|
2020-06-15 18:15:10 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto ptr = src.lock())
|
|
|
|
if (auto maybe_addr = ptr->BoundOn())
|
|
|
|
addrs.emplace_back(*maybe_addr);
|
2020-06-15 18:15:10 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
return addrs;
|
|
|
|
}
|
2020-06-15 18:15:10 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
std::optional<SockAddr>
|
|
|
|
Server::FirstBoundPacketSourceAddr() const
|
|
|
|
{
|
|
|
|
for (const auto& src : m_PacketSources)
|
2018-12-03 22:22:59 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto ptr = src.lock())
|
|
|
|
if (auto bound = ptr->BoundOn())
|
|
|
|
return bound;
|
2019-05-22 16:20:03 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
return std::nullopt;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
Server::AddResolver(std::weak_ptr<Resolver_Base> resolver)
|
|
|
|
{
|
|
|
|
m_Resolvers.insert(resolver);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
Server::AddResolver(std::shared_ptr<Resolver_Base> resolver)
|
|
|
|
{
|
|
|
|
m_OwnedResolvers.insert(resolver);
|
|
|
|
AddResolver(std::weak_ptr<Resolver_Base>{resolver});
|
|
|
|
}
|
2019-05-22 16:20:03 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
void
|
|
|
|
Server::AddPacketSource(std::weak_ptr<PacketSource_Base> pkt)
|
|
|
|
{
|
|
|
|
m_PacketSources.push_back(pkt);
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2020-06-15 18:15:10 +00:00
|
|
|
|
2021-03-02 18:18:22 +00:00
|
|
|
void
|
2022-04-07 20:44:23 +00:00
|
|
|
Server::AddPacketSource(std::shared_ptr<PacketSource_Base> pkt)
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
AddPacketSource(std::weak_ptr<PacketSource_Base>{pkt});
|
2022-09-12 22:11:25 +00:00
|
|
|
m_OwnedPacketSources.push_back(std::move(pkt));
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
Server::Stop()
|
|
|
|
{
|
|
|
|
for (const auto& resolver : m_Resolvers)
|
2019-05-22 16:20:03 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto ptr = resolver.lock())
|
2022-09-16 23:55:59 +00:00
|
|
|
ptr->Down();
|
2018-12-03 22:22:59 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
2018-12-03 22:22:59 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
void
|
|
|
|
Server::Reset()
|
|
|
|
{
|
|
|
|
for (const auto& resolver : m_Resolvers)
|
2018-12-03 22:22:59 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto ptr = resolver.lock())
|
2022-09-16 23:55:59 +00:00
|
|
|
ptr->ResetResolver();
|
2018-12-04 16:35:25 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
}
|
2018-12-04 16:16:43 +00:00
|
|
|
|
2022-06-22 16:14:33 +00:00
|
|
|
void
|
|
|
|
Server::SetDNSMode(bool all_queries)
|
|
|
|
{
|
|
|
|
if (auto maybe_addr = FirstBoundPacketSourceAddr())
|
2022-07-28 16:07:38 +00:00
|
|
|
m_Platform->set_resolver(m_NetIfIndex, *maybe_addr, all_queries);
|
2022-06-22 16:14:33 +00:00
|
|
|
}
|
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
bool
|
|
|
|
Server::MaybeHandlePacket(
|
2022-07-28 16:07:38 +00:00
|
|
|
std::shared_ptr<PacketSource_Base> ptr,
|
2022-04-07 20:44:23 +00:00
|
|
|
const SockAddr& to,
|
|
|
|
const SockAddr& from,
|
|
|
|
llarp::OwnedBuffer buf)
|
|
|
|
{
|
|
|
|
// dont process to prevent feedback loop
|
|
|
|
if (ptr->WouldLoop(to, from))
|
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
log::warning(logcat, "preventing dns packet replay to={} from={}", to, from);
|
2022-04-07 20:44:23 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto maybe = MaybeParseDNSMessage(buf);
|
|
|
|
if (not maybe)
|
|
|
|
{
|
2022-09-12 16:17:22 +00:00
|
|
|
log::warning(logcat, "invalid dns message format from {} to dns listener on {}", from, to);
|
2022-04-07 20:44:23 +00:00
|
|
|
return false;
|
|
|
|
}
|
2022-07-28 16:07:38 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
auto& msg = *maybe;
|
2021-03-02 18:18:22 +00:00
|
|
|
// we don't provide a DoH resolver because it requires verified TLS
|
|
|
|
// TLS needs X509/ASN.1-DER and opting into the Root CA Cabal
|
|
|
|
// thankfully mozilla added a backdoor that allows ISPs to turn it off
|
|
|
|
// so we disable DoH for firefox using mozilla's ISP backdoor
|
2022-04-07 20:44:23 +00:00
|
|
|
// see: https://github.com/oxen-io/lokinet/issues/832
|
2021-03-02 18:18:22 +00:00
|
|
|
for (const auto& q : msg.questions)
|
2018-12-04 16:35:25 +00:00
|
|
|
{
|
2021-03-02 18:18:22 +00:00
|
|
|
// is this firefox looking for their backdoor record?
|
|
|
|
if (q.IsName("use-application-dns.net"))
|
2018-12-04 16:35:25 +00:00
|
|
|
{
|
2021-03-02 18:18:22 +00:00
|
|
|
// yea it is, let's turn off DoH because god is dead.
|
|
|
|
msg.AddNXReply();
|
2022-04-07 20:44:23 +00:00
|
|
|
// press F to pay respects and send it back where it came from
|
|
|
|
ptr->SendTo(from, to, msg.ToBuffer());
|
|
|
|
return true;
|
2018-12-04 16:35:25 +00:00
|
|
|
}
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2019-11-01 13:40:31 +00:00
|
|
|
|
2022-04-07 20:44:23 +00:00
|
|
|
for (const auto& resolver : m_Resolvers)
|
2021-03-02 18:18:22 +00:00
|
|
|
{
|
2022-04-07 20:44:23 +00:00
|
|
|
if (auto res_ptr = resolver.lock())
|
2018-12-03 22:22:59 +00:00
|
|
|
{
|
2022-11-22 21:27:21 +00:00
|
|
|
log::trace(
|
2022-09-12 16:17:22 +00:00
|
|
|
logcat, "check resolver {} for dns from {} to {}", res_ptr->ResolverName(), from, to);
|
2022-07-28 16:07:38 +00:00
|
|
|
if (res_ptr->MaybeHookDNS(ptr, msg, to, from))
|
2022-11-22 21:27:21 +00:00
|
|
|
{
|
|
|
|
log::trace(
|
|
|
|
logcat, "resolver {} handling dns from {} to {}", res_ptr->ResolverName(), from, to);
|
2022-04-07 20:44:23 +00:00
|
|
|
return true;
|
2022-11-22 21:27:21 +00:00
|
|
|
}
|
2018-12-03 22:22:59 +00:00
|
|
|
}
|
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
return false;
|
2021-03-02 18:18:22 +00:00
|
|
|
}
|
2022-04-07 20:44:23 +00:00
|
|
|
|
2021-03-02 18:18:22 +00:00
|
|
|
} // namespace llarp::dns
|