2018-12-12 01:06:46 +00:00
|
|
|
#include <exit/session.hpp>
|
2019-01-16 00:24:16 +00:00
|
|
|
|
2019-05-28 19:45:08 +00:00
|
|
|
#include <crypto/crypto.hpp>
|
2019-02-11 19:45:42 +00:00
|
|
|
#include <nodedb.hpp>
|
2019-06-17 23:19:39 +00:00
|
|
|
#include <path/path_context.hpp>
|
2019-01-11 01:19:36 +00:00
|
|
|
#include <path/path.hpp>
|
2019-02-11 19:45:42 +00:00
|
|
|
#include <router/abstractrouter.hpp>
|
2019-09-01 12:38:03 +00:00
|
|
|
#include <util/meta/memfn.hpp>
|
2019-07-30 23:42:13 +00:00
|
|
|
#include <utility>
|
2018-11-12 16:43:40 +00:00
|
|
|
|
|
|
|
namespace llarp
|
|
|
|
{
|
|
|
|
namespace exit
|
|
|
|
{
|
2019-02-01 01:58:06 +00:00
|
|
|
BaseSession::BaseSession(
|
2019-08-02 09:27:27 +00:00
|
|
|
const llarp::RouterID& routerId,
|
2019-02-11 19:45:42 +00:00
|
|
|
std::function< bool(const llarp_buffer_t&) > writepkt,
|
2019-05-02 18:11:44 +00:00
|
|
|
AbstractRouter* r, size_t numpaths, size_t hoplen, bool bundleRC)
|
2019-06-20 16:22:29 +00:00
|
|
|
: llarp::path::Builder(r, numpaths, hoplen)
|
2019-08-02 09:27:27 +00:00
|
|
|
, m_ExitRouter(routerId)
|
2019-07-30 23:42:13 +00:00
|
|
|
, m_WritePacket(std::move(writepkt))
|
2018-11-29 21:19:20 +00:00
|
|
|
, m_Counter(0)
|
2020-01-08 16:05:04 +00:00
|
|
|
, m_LastUse(r->Now())
|
2019-05-02 18:11:44 +00:00
|
|
|
, m_BundleRC(bundleRC)
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
2019-05-28 19:45:08 +00:00
|
|
|
CryptoManager::instance()->identity_keygen(m_ExitIdentity);
|
2018-11-14 19:34:17 +00:00
|
|
|
}
|
|
|
|
|
2019-07-30 23:42:13 +00:00
|
|
|
BaseSession::~BaseSession() = default;
|
2018-11-14 19:34:17 +00:00
|
|
|
|
2019-05-06 14:54:05 +00:00
|
|
|
void
|
|
|
|
BaseSession::HandlePathDied(path::Path_ptr p)
|
2019-03-30 13:02:10 +00:00
|
|
|
{
|
2019-05-06 14:54:05 +00:00
|
|
|
p->Rebuild();
|
2019-03-30 13:02:10 +00:00
|
|
|
}
|
|
|
|
|
2019-02-11 17:14:43 +00:00
|
|
|
util::StatusObject
|
|
|
|
BaseSession::ExtractStatus() const
|
2019-02-08 19:43:25 +00:00
|
|
|
{
|
2019-08-19 09:33:26 +00:00
|
|
|
auto obj = path::Builder::ExtractStatus();
|
|
|
|
obj["lastExitUse"] = m_LastUse;
|
|
|
|
auto pub = m_ExitIdentity.toPublic();
|
|
|
|
obj["exitIdentity"] = pub.ToString();
|
2019-02-11 17:14:43 +00:00
|
|
|
return obj;
|
2019-02-08 19:43:25 +00:00
|
|
|
}
|
|
|
|
|
2018-12-13 16:14:44 +00:00
|
|
|
bool
|
|
|
|
BaseSession::LoadIdentityFromFile(const char* fname)
|
|
|
|
{
|
|
|
|
return m_ExitIdentity.LoadFromFile(fname);
|
|
|
|
}
|
|
|
|
|
2018-11-16 14:03:13 +00:00
|
|
|
bool
|
|
|
|
BaseSession::ShouldBuildMore(llarp_time_t now) const
|
|
|
|
{
|
2019-07-18 16:28:17 +00:00
|
|
|
const size_t expect = (1 + (numPaths / 2));
|
2018-12-27 12:00:28 +00:00
|
|
|
// check 30 seconds into the future and see if we need more paths
|
2019-04-23 18:29:42 +00:00
|
|
|
const llarp_time_t future = now + (30 * 1000) + buildIntervalLimit;
|
2019-05-06 14:21:47 +00:00
|
|
|
return NumPathsExistingAt(future) < expect && !BuildCooldownHit(now);
|
2018-11-16 14:03:13 +00:00
|
|
|
}
|
|
|
|
|
2019-05-10 16:19:33 +00:00
|
|
|
void
|
|
|
|
BaseSession::BlacklistSnode(const RouterID snode)
|
|
|
|
{
|
|
|
|
m_SnodeBlacklist.insert(std::move(snode));
|
|
|
|
}
|
|
|
|
|
2018-11-14 19:34:17 +00:00
|
|
|
bool
|
2019-05-08 14:01:31 +00:00
|
|
|
BaseSession::SelectHop(llarp_nodedb* db, const std::set< RouterID >& prev,
|
2018-11-14 19:34:17 +00:00
|
|
|
RouterContact& cur, size_t hop,
|
|
|
|
llarp::path::PathRole roles)
|
|
|
|
{
|
2019-05-08 14:01:31 +00:00
|
|
|
std::set< RouterID > exclude = prev;
|
2019-05-10 16:19:33 +00:00
|
|
|
for(const auto& snode : m_SnodeBlacklist)
|
|
|
|
{
|
|
|
|
if(snode != m_ExitRouter)
|
|
|
|
exclude.insert(snode);
|
|
|
|
}
|
2019-05-08 14:01:31 +00:00
|
|
|
exclude.insert(m_ExitRouter);
|
2018-11-14 19:34:17 +00:00
|
|
|
if(hop == numHops - 1)
|
2018-12-10 15:44:18 +00:00
|
|
|
{
|
2019-04-30 21:36:27 +00:00
|
|
|
if(db->Get(m_ExitRouter, cur))
|
|
|
|
return true;
|
2019-08-02 09:27:27 +00:00
|
|
|
m_router->LookupRouter(m_ExitRouter, nullptr);
|
2019-04-30 21:36:27 +00:00
|
|
|
return false;
|
2018-12-10 15:44:18 +00:00
|
|
|
}
|
2019-07-06 17:03:40 +00:00
|
|
|
|
|
|
|
return path::Builder::SelectHop(db, exclude, cur, hop, roles);
|
2018-11-14 19:34:17 +00:00
|
|
|
}
|
|
|
|
|
2019-04-16 19:39:58 +00:00
|
|
|
bool
|
2019-04-23 14:28:59 +00:00
|
|
|
BaseSession::CheckPathDead(path::Path_ptr, llarp_time_t dlt)
|
2019-04-16 19:39:58 +00:00
|
|
|
{
|
|
|
|
return dlt >= 10000;
|
|
|
|
}
|
|
|
|
|
2018-11-14 19:34:17 +00:00
|
|
|
void
|
2019-04-23 14:28:59 +00:00
|
|
|
BaseSession::HandlePathBuilt(llarp::path::Path_ptr p)
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
2018-12-10 15:31:58 +00:00
|
|
|
path::Builder::HandlePathBuilt(p);
|
2019-06-02 21:19:10 +00:00
|
|
|
p->SetDropHandler(util::memFn(&BaseSession::HandleTrafficDrop, this));
|
|
|
|
p->SetDeadChecker(util::memFn(&BaseSession::CheckPathDead, this));
|
|
|
|
p->SetExitTrafficHandler(util::memFn(&BaseSession::HandleTraffic, this));
|
|
|
|
p->AddObtainExitHandler(util::memFn(&BaseSession::HandleGotExit, this));
|
2018-12-20 12:41:17 +00:00
|
|
|
|
2019-05-28 19:45:08 +00:00
|
|
|
routing::ObtainExitMessage obtain;
|
2018-11-14 19:34:17 +00:00
|
|
|
obtain.S = p->NextSeqNo();
|
2018-12-11 00:53:11 +00:00
|
|
|
obtain.T = llarp::randint();
|
2018-11-29 13:12:35 +00:00
|
|
|
PopulateRequest(obtain);
|
2019-05-28 19:45:08 +00:00
|
|
|
if(!obtain.Sign(m_ExitIdentity))
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
|
|
|
llarp::LogError("Failed to sign exit request");
|
|
|
|
return;
|
|
|
|
}
|
2019-08-02 09:27:27 +00:00
|
|
|
if(p->SendExitRequest(obtain, m_router))
|
2018-11-14 19:34:17 +00:00
|
|
|
llarp::LogInfo("asking ", m_ExitRouter, " for exit");
|
|
|
|
else
|
2019-01-16 00:24:16 +00:00
|
|
|
llarp::LogError("failed to send exit request");
|
2018-11-14 19:34:17 +00:00
|
|
|
}
|
|
|
|
|
2019-03-01 19:10:42 +00:00
|
|
|
void
|
|
|
|
BaseSession::AddReadyHook(SessionReadyFunc func)
|
|
|
|
{
|
|
|
|
m_PendingCallbacks.emplace_back(func);
|
|
|
|
}
|
|
|
|
|
2018-11-14 19:34:17 +00:00
|
|
|
bool
|
2019-04-23 14:28:59 +00:00
|
|
|
BaseSession::HandleGotExit(llarp::path::Path_ptr p, llarp_time_t b)
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
|
|
|
if(b == 0)
|
2019-04-30 21:36:27 +00:00
|
|
|
{
|
2018-11-14 19:34:17 +00:00
|
|
|
llarp::LogInfo("obtained an exit via ", p->Endpoint());
|
2019-03-07 15:17:29 +00:00
|
|
|
CallPendingCallbacks(true);
|
2019-04-30 21:36:27 +00:00
|
|
|
}
|
2019-03-07 15:17:29 +00:00
|
|
|
return true;
|
|
|
|
}
|
2018-12-13 12:27:14 +00:00
|
|
|
|
2019-03-07 15:17:29 +00:00
|
|
|
void
|
|
|
|
BaseSession::CallPendingCallbacks(bool success)
|
|
|
|
{
|
|
|
|
if(success)
|
|
|
|
{
|
2019-04-23 16:13:22 +00:00
|
|
|
auto self = shared_from_this();
|
2019-03-07 15:17:29 +00:00
|
|
|
for(auto& f : m_PendingCallbacks)
|
2019-04-23 16:13:22 +00:00
|
|
|
f(self);
|
2019-03-07 15:17:29 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
for(auto& f : m_PendingCallbacks)
|
|
|
|
f(nullptr);
|
|
|
|
}
|
2019-03-01 19:10:42 +00:00
|
|
|
m_PendingCallbacks.clear();
|
2018-11-14 19:34:17 +00:00
|
|
|
}
|
|
|
|
|
2019-05-07 18:15:22 +00:00
|
|
|
void
|
|
|
|
BaseSession::ResetInternalState()
|
|
|
|
{
|
|
|
|
auto sendExitClose = [&](const llarp::path::Path_ptr p) {
|
2019-05-08 12:50:33 +00:00
|
|
|
const static auto roles =
|
|
|
|
llarp::path::ePathRoleExit | llarp::path::ePathRoleSVC;
|
|
|
|
if(p->SupportsAnyRoles(roles))
|
2019-05-07 18:15:22 +00:00
|
|
|
{
|
|
|
|
llarp::LogInfo(p->Name(), " closing exit path");
|
2019-05-28 19:45:08 +00:00
|
|
|
routing::CloseExitMessage msg;
|
2019-08-02 09:27:27 +00:00
|
|
|
if(msg.Sign(m_ExitIdentity) && p->SendExitClose(msg, m_router))
|
2019-05-08 12:50:33 +00:00
|
|
|
{
|
|
|
|
p->ClearRoles(roles);
|
|
|
|
}
|
|
|
|
else
|
2019-05-07 18:15:22 +00:00
|
|
|
llarp::LogWarn(p->Name(), " failed to send exit close message");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
ForEachPath(sendExitClose);
|
|
|
|
path::Builder::ResetInternalState();
|
|
|
|
}
|
|
|
|
|
2018-12-24 16:09:05 +00:00
|
|
|
bool
|
|
|
|
BaseSession::Stop()
|
|
|
|
{
|
2019-03-07 15:17:29 +00:00
|
|
|
CallPendingCallbacks(false);
|
2019-05-28 19:45:08 +00:00
|
|
|
auto sendExitClose = [&](const path::Path_ptr p) {
|
|
|
|
if(p->SupportsAnyRoles(path::ePathRoleExit))
|
2018-12-24 16:09:05 +00:00
|
|
|
{
|
2019-05-28 19:45:08 +00:00
|
|
|
LogInfo(p->Name(), " closing exit path");
|
|
|
|
routing::CloseExitMessage msg;
|
2019-08-02 09:27:27 +00:00
|
|
|
if(!(msg.Sign(m_ExitIdentity) && p->SendExitClose(msg, m_router)))
|
2019-05-28 19:45:08 +00:00
|
|
|
LogWarn(p->Name(), " failed to send exit close message");
|
2018-12-24 16:09:05 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
ForEachPath(sendExitClose);
|
2019-08-02 09:27:27 +00:00
|
|
|
m_router->pathContext().RemovePathSet(shared_from_this());
|
2019-05-28 19:45:08 +00:00
|
|
|
return path::Builder::Stop();
|
2018-12-24 16:09:05 +00:00
|
|
|
}
|
|
|
|
|
2018-11-14 19:34:17 +00:00
|
|
|
bool
|
2019-04-23 14:28:59 +00:00
|
|
|
BaseSession::HandleTraffic(llarp::path::Path_ptr, const llarp_buffer_t& buf,
|
2018-12-20 12:41:17 +00:00
|
|
|
uint64_t counter)
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
|
|
|
if(m_WritePacket)
|
2018-12-13 12:27:14 +00:00
|
|
|
{
|
2019-06-11 16:44:05 +00:00
|
|
|
llarp::net::IPPacket pkt;
|
2018-12-20 12:41:17 +00:00
|
|
|
if(!pkt.Load(buf))
|
2018-12-13 12:27:14 +00:00
|
|
|
return false;
|
2019-08-02 09:27:27 +00:00
|
|
|
m_LastUse = m_router->Now();
|
2020-01-08 16:03:52 +00:00
|
|
|
m_Downstream.emplace(counter, pkt);
|
2018-12-13 12:27:14 +00:00
|
|
|
return true;
|
|
|
|
}
|
2018-11-14 19:34:17 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
2019-05-31 10:57:41 +00:00
|
|
|
BaseSession::HandleTrafficDrop(llarp::path::Path_ptr p,
|
|
|
|
const PathID_t& path, uint64_t s)
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
|
|
|
llarp::LogError("dropped traffic on exit ", m_ExitRouter, " S=", s,
|
|
|
|
" P=", path);
|
2019-08-02 09:27:27 +00:00
|
|
|
p->EnterState(path::ePathIgnore, m_router->Now());
|
2018-11-14 19:34:17 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
2019-06-11 16:44:05 +00:00
|
|
|
BaseSession::QueueUpstreamTraffic(llarp::net::IPPacket pkt, const size_t N)
|
2018-11-28 16:38:20 +00:00
|
|
|
{
|
2019-10-28 19:32:16 +00:00
|
|
|
const auto pktbuf = pkt.ConstBuffer();
|
|
|
|
const llarp_buffer_t& buf = pktbuf;
|
2019-02-02 23:12:42 +00:00
|
|
|
auto& queue = m_Upstream[buf.sz / N];
|
2018-11-29 13:12:35 +00:00
|
|
|
// queue overflow
|
2018-11-29 15:45:27 +00:00
|
|
|
if(queue.size() >= MaxUpstreamQueueLength)
|
2018-11-29 13:12:35 +00:00
|
|
|
return false;
|
2018-11-29 15:45:27 +00:00
|
|
|
if(queue.size() == 0)
|
|
|
|
{
|
|
|
|
queue.emplace_back();
|
2018-11-29 21:19:20 +00:00
|
|
|
return queue.back().PutBuffer(buf, m_Counter++);
|
2018-11-29 15:45:27 +00:00
|
|
|
}
|
2018-12-02 18:07:07 +00:00
|
|
|
auto& back = queue.back();
|
2018-11-28 16:38:20 +00:00
|
|
|
// pack to nearest N
|
|
|
|
if(back.Size() + buf.sz > N)
|
|
|
|
{
|
2018-12-02 18:07:07 +00:00
|
|
|
queue.emplace_back();
|
|
|
|
return queue.back().PutBuffer(buf, m_Counter++);
|
2018-11-28 16:38:20 +00:00
|
|
|
}
|
2019-07-06 17:03:40 +00:00
|
|
|
|
|
|
|
return back.PutBuffer(buf, m_Counter++);
|
2018-11-28 16:38:20 +00:00
|
|
|
}
|
|
|
|
|
2018-11-29 13:12:35 +00:00
|
|
|
bool
|
2018-12-02 18:07:07 +00:00
|
|
|
BaseSession::IsReady() const
|
2018-11-29 13:12:35 +00:00
|
|
|
{
|
2019-07-18 16:28:17 +00:00
|
|
|
const size_t expect = (1 + (numPaths / 2));
|
2019-03-07 15:17:29 +00:00
|
|
|
return AvailablePaths(llarp::path::ePathRoleExit) >= expect;
|
2018-11-29 13:12:35 +00:00
|
|
|
}
|
|
|
|
|
2018-12-13 12:27:14 +00:00
|
|
|
bool
|
|
|
|
BaseSession::IsExpired(llarp_time_t now) const
|
|
|
|
{
|
2020-01-08 16:05:29 +00:00
|
|
|
return now > m_LastUse && now - m_LastUse > LifeSpan;
|
2018-12-13 12:27:14 +00:00
|
|
|
}
|
|
|
|
|
2019-05-07 15:08:57 +00:00
|
|
|
bool
|
|
|
|
BaseSession::UrgentBuild(llarp_time_t now) const
|
|
|
|
{
|
|
|
|
if(!IsReady())
|
2019-07-18 16:28:17 +00:00
|
|
|
return NumInStatus(path::ePathBuilding) < numPaths;
|
2019-05-07 15:08:57 +00:00
|
|
|
return path::Builder::UrgentBuild(now);
|
|
|
|
}
|
|
|
|
|
2018-12-02 18:07:07 +00:00
|
|
|
bool
|
2019-04-30 13:56:39 +00:00
|
|
|
BaseSession::FlushUpstream()
|
2018-11-14 19:34:17 +00:00
|
|
|
{
|
2019-08-02 09:27:27 +00:00
|
|
|
auto now = m_router->Now();
|
2018-11-14 19:34:17 +00:00
|
|
|
auto path = PickRandomEstablishedPath(llarp::path::ePathRoleExit);
|
2018-12-27 12:00:28 +00:00
|
|
|
if(path)
|
2018-11-28 16:38:20 +00:00
|
|
|
{
|
2018-12-02 18:07:07 +00:00
|
|
|
for(auto& item : m_Upstream)
|
2018-11-29 15:45:27 +00:00
|
|
|
{
|
2019-01-19 01:04:47 +00:00
|
|
|
auto& queue = item.second; // XXX: uninitialised memory here!
|
2018-12-27 12:00:28 +00:00
|
|
|
while(queue.size())
|
|
|
|
{
|
|
|
|
auto& msg = queue.front();
|
2019-05-22 16:20:03 +00:00
|
|
|
if(path)
|
|
|
|
{
|
2019-05-22 16:20:50 +00:00
|
|
|
msg.S = path->NextSeqNo();
|
2020-01-08 16:03:52 +00:00
|
|
|
path->SendRoutingMessage(msg, m_router);
|
2019-05-22 16:20:03 +00:00
|
|
|
}
|
2018-12-27 12:00:28 +00:00
|
|
|
queue.pop_front();
|
2019-05-22 16:20:03 +00:00
|
|
|
|
2019-05-06 17:32:53 +00:00
|
|
|
// spread across all paths
|
|
|
|
path = PickRandomEstablishedPath(llarp::path::ePathRoleExit);
|
2018-12-27 12:00:28 +00:00
|
|
|
}
|
2018-11-29 15:45:27 +00:00
|
|
|
}
|
2018-11-28 16:38:20 +00:00
|
|
|
}
|
2018-12-27 12:00:28 +00:00
|
|
|
else
|
|
|
|
{
|
|
|
|
if(m_Upstream.size())
|
|
|
|
llarp::LogWarn("no path for exit session");
|
|
|
|
// discard upstream
|
|
|
|
for(auto& item : m_Upstream)
|
|
|
|
item.second.clear();
|
|
|
|
m_Upstream.clear();
|
2019-05-03 13:15:03 +00:00
|
|
|
if(numHops == 1)
|
|
|
|
{
|
2019-08-02 09:27:27 +00:00
|
|
|
auto r = m_router;
|
2019-05-03 17:42:48 +00:00
|
|
|
RouterContact rc;
|
|
|
|
if(r->nodedb()->Get(m_ExitRouter, rc))
|
|
|
|
r->TryConnectAsync(rc, 5);
|
|
|
|
else
|
|
|
|
r->LookupRouter(m_ExitRouter,
|
2019-05-05 13:51:48 +00:00
|
|
|
[r](const std::vector< RouterContact >& results) {
|
|
|
|
if(results.size())
|
|
|
|
r->TryConnectAsync(results[0], 5);
|
|
|
|
});
|
2019-05-03 13:15:03 +00:00
|
|
|
}
|
2019-05-07 17:27:32 +00:00
|
|
|
else if(UrgentBuild(now))
|
2019-05-07 12:34:02 +00:00
|
|
|
BuildOneAlignedTo(m_ExitRouter);
|
2018-12-27 12:00:28 +00:00
|
|
|
}
|
2019-04-30 13:56:39 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
BaseSession::FlushDownstream()
|
|
|
|
{
|
2018-12-20 12:41:17 +00:00
|
|
|
while(m_Downstream.size())
|
|
|
|
{
|
|
|
|
if(m_WritePacket)
|
|
|
|
m_WritePacket(m_Downstream.top().second.ConstBuffer());
|
|
|
|
m_Downstream.pop();
|
|
|
|
}
|
2018-11-14 19:34:17 +00:00
|
|
|
}
|
|
|
|
|
2019-02-01 01:58:06 +00:00
|
|
|
SNodeSession::SNodeSession(
|
|
|
|
const llarp::RouterID& snodeRouter,
|
2019-02-11 19:45:42 +00:00
|
|
|
std::function< bool(const llarp_buffer_t&) > writepkt,
|
|
|
|
AbstractRouter* r, size_t numpaths, size_t hoplen,
|
2019-05-02 18:11:44 +00:00
|
|
|
bool useRouterSNodeKey, bool bundleRC)
|
|
|
|
: BaseSession(snodeRouter, writepkt, r, numpaths, hoplen, bundleRC)
|
2018-12-13 16:14:44 +00:00
|
|
|
{
|
|
|
|
if(useRouterSNodeKey)
|
|
|
|
{
|
2019-02-11 19:45:42 +00:00
|
|
|
m_ExitIdentity = r->identity();
|
2018-12-13 16:14:44 +00:00
|
|
|
}
|
|
|
|
}
|
2019-03-22 14:10:30 +00:00
|
|
|
|
|
|
|
std::string
|
|
|
|
SNodeSession::Name() const
|
|
|
|
{
|
|
|
|
return "SNode::" + m_ExitRouter.ToString();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string
|
|
|
|
ExitSession::Name() const
|
|
|
|
{
|
|
|
|
return "Exit::" + m_ExitRouter.ToString();
|
|
|
|
}
|
2018-11-14 19:34:17 +00:00
|
|
|
} // namespace exit
|
2018-12-10 15:31:58 +00:00
|
|
|
} // namespace llarp
|