lokinet/llarp/path/path.cpp

714 lines
20 KiB
C++
Raw Normal View History

2019-01-11 01:19:36 +00:00
#include <path/path.hpp>
2019-06-15 14:55:14 +00:00
#include <exit/exit_messages.hpp>
#include <messages/discard.hpp>
#include <messages/relay_commit.hpp>
#include <messages/relay_status.hpp>
2019-01-11 01:19:36 +00:00
#include <path/pathbuilder.hpp>
2019-06-17 23:19:39 +00:00
#include <path/transit_hop.hpp>
#include <profiling.hpp>
#include <router/abstractrouter.hpp>
#include <routing/dht_message.hpp>
2019-06-19 22:30:07 +00:00
#include <routing/path_latency_message.hpp>
#include <routing/transfer_traffic_message.hpp>
#include <util/buffer.hpp>
#include <util/endian.hpp>
2018-06-12 16:45:12 +00:00
#include <deque>
2018-06-10 14:05:48 +00:00
namespace llarp
{
namespace path
2018-06-10 14:05:48 +00:00
{
2018-11-14 18:02:27 +00:00
Path::Path(const std::vector< RouterContact >& h, PathSet* parent,
PathRole startingRoles)
: m_PathSet(parent), _role(startingRoles)
{
2018-09-06 11:46:19 +00:00
hops.resize(h.size());
2018-08-30 18:48:43 +00:00
size_t hsz = h.size();
for(size_t idx = 0; idx < hsz; ++idx)
{
2018-08-30 18:48:43 +00:00
hops[idx].rc = h[idx];
hops[idx].txID.Randomize();
hops[idx].rxID.Randomize();
}
2018-08-30 18:48:43 +00:00
for(size_t idx = 0; idx < hsz - 1; ++idx)
{
hops[idx].txID = hops[idx + 1].rxID;
}
// initialize parts of the introduction
2018-08-30 18:48:43 +00:00
intro.router = hops[hsz - 1].rc.pubkey;
2018-10-06 16:37:54 +00:00
intro.pathID = hops[hsz - 1].txID;
2018-10-29 16:48:36 +00:00
EnterState(ePathBuilding, parent->Now());
}
2018-06-22 00:25:30 +00:00
void
Path::SetBuildResultHook(BuildResultHookFunc func)
{
m_BuiltHook = func;
}
2018-06-22 00:25:30 +00:00
RouterID
Path::Endpoint() const
{
2018-08-30 18:48:43 +00:00
return hops[hops.size() - 1].rc.pubkey;
}
PubKey
Path::EndpointPubKey() const
{
return hops[hops.size() - 1].rc.pubkey;
}
2018-12-18 19:03:50 +00:00
PathID_t
Path::TXID() const
2018-06-22 00:25:30 +00:00
{
return hops[0].txID;
2018-06-22 00:25:30 +00:00
}
2018-06-10 14:05:48 +00:00
2018-12-18 19:03:50 +00:00
PathID_t
Path::RXID() const
{
return hops[0].rxID;
}
2018-06-19 17:11:24 +00:00
bool
Path::IsReady() const
{
return intro.latency > 0 && _status == ePathEstablished;
}
2019-03-08 17:26:29 +00:00
bool
Path::IsEndpoint(const RouterID& r, const PathID_t& id) const
{
return hops[hops.size() - 1].rc.pubkey == r
&& hops[hops.size() - 1].txID == id;
}
RouterID
Path::Upstream() const
{
2018-08-30 18:48:43 +00:00
return hops[0].rc.pubkey;
}
std::string
Path::HopsString() const
{
std::stringstream ss;
for(const auto& hop : hops)
ss << RouterID(hop.rc.pubkey) << " -> ";
return ss.str();
}
bool
Path::HandleLRSM(uint64_t status, std::array< EncryptedFrame, 8 >& frames,
AbstractRouter* r)
{
uint64_t currentStatus = LR_StatusRecord::SUCCESS;
size_t index = 0;
while(index < hops.size())
{
if(!frames[index].DoDecrypt(hops[index].shared))
{
currentStatus = LR_StatusRecord::FAIL_DECRYPT_ERROR;
break;
}
llarp::LogDebug("decrypted LRSM frame from ", hops[index].rc.pubkey);
llarp_buffer_t* buf = frames[index].Buffer();
buf->cur = buf->base + EncryptedFrameOverheadSize;
LR_StatusRecord record;
// successful decrypt
if(!record.BDecode(buf))
{
llarp::LogWarn("malformed frame inside LRCM from ",
hops[index].rc.pubkey);
currentStatus = LR_StatusRecord::FAIL_MALFORMED_RECORD;
break;
}
llarp::LogDebug("Decoded LR Status Record from ",
hops[index].rc.pubkey);
currentStatus = record.status;
if((currentStatus & LR_StatusRecord::SUCCESS) == 0)
{
break;
}
++index;
}
if((currentStatus & LR_StatusRecord::SUCCESS) == 1)
{
llarp::LogDebug("LR_Status message processed, path build successful");
HandlePathConfirmMessage(r);
}
else
{
r->routerProfiling().MarkPathFail(this);
llarp::LogInfo("LR_Status message processed, path build failed");
if(currentStatus & LR_StatusRecord::FAIL_TIMEOUT)
{
llarp::LogDebug("Path build failed due to timeout");
}
else if(currentStatus & LR_StatusRecord::FAIL_CONGESTION)
{
llarp::LogDebug("Path build failed due to congestion");
}
else if(currentStatus & LR_StatusRecord::FAIL_DEST_UNKNOWN)
{
llarp::LogDebug(
"Path build failed due to one or more nodes giving destination "
"unknown");
}
else if(currentStatus & LR_StatusRecord::FAIL_DEST_INVALID)
{
llarp::LogDebug(
"Path build failed due to one or more nodes considered an "
"invalid destination");
}
else if(currentStatus & LR_StatusRecord::FAIL_CANNOT_CONNECT)
{
llarp::LogDebug(
"Path build failed due to a node being unable to connect to the "
"next hop");
}
else if(currentStatus & LR_StatusRecord::FAIL_MALFORMED_RECORD)
{
llarp::LogDebug(
"Path build failed due to a malformed record in the build status "
"message");
}
else if(currentStatus & LR_StatusRecord::FAIL_DECRYPT_ERROR)
{
llarp::LogDebug(
"Path build failed due to a decrypt error in the build status "
"message");
}
else
{
llarp::LogDebug("Path build failed for an unspecified reason");
}
EnterState(ePathFailed, r->Now());
}
// TODO: meaningful return value?
return true;
}
void
2018-10-29 16:48:36 +00:00
Path::EnterState(PathStatus st, llarp_time_t now)
{
if(st == ePathFailed)
{
_status = st;
}
else if(st == ePathExpired && _status == ePathBuilding)
{
2019-04-13 14:32:07 +00:00
_status = st;
m_PathSet->HandlePathBuildTimeout(shared_from_this());
}
else if(st == ePathBuilding)
{
LogInfo("path ", Name(), " is building");
2018-10-29 16:48:36 +00:00
buildStarted = now;
}
else if(st == ePathEstablished && _status == ePathBuilding)
{
LogInfo("path ", Name(), " is built, took ", now - buildStarted, " ms");
}
2019-03-30 13:02:10 +00:00
else if(st == ePathTimeout && _status == ePathEstablished)
{
2019-03-30 13:12:48 +00:00
LogInfo("path ", Name(), " died");
_status = st;
m_PathSet->HandlePathDied(shared_from_this());
2019-03-30 13:02:10 +00:00
}
2019-04-13 14:32:07 +00:00
else if(st == ePathEstablished && _status == ePathTimeout)
{
LogInfo("path ", Name(), " reanimated");
}
_status = st;
}
2019-02-11 17:14:43 +00:00
util::StatusObject
PathHopConfig::ExtractStatus() const
2019-02-08 19:43:25 +00:00
{
2019-02-11 17:14:43 +00:00
util::StatusObject obj{{"lifetime", lifetime},
{"router", rc.pubkey.ToHex()},
{"txid", txID.ToHex()},
{"rxid", rxID.ToHex()}};
return obj;
}
2019-02-08 19:43:25 +00:00
2019-02-11 17:14:43 +00:00
util::StatusObject
Path::ExtractStatus() const
{
auto now = llarp::time_now_ms();
util::StatusObject obj{{"intro", intro.ExtractStatus()},
{"lastRecvMsg", m_LastRecvMessage},
{"lastLatencyTest", m_LastLatencyTestTime},
{"buildStarted", buildStarted},
{"expired", Expired(now)},
{"expiresSoon", ExpiresSoon(now)},
{"expiresAt", ExpireTime()},
{"ready", IsReady()},
{"hasExit", SupportsAnyRoles(ePathRoleExit)}};
std::vector< util::StatusObject > hopsObj;
std::transform(hops.begin(), hops.end(), std::back_inserter(hopsObj),
[](const auto& hop) -> util::StatusObject {
return hop.ExtractStatus();
});
obj.Put("hops", hopsObj);
2019-02-08 19:43:25 +00:00
switch(_status)
{
case ePathBuilding:
2019-02-11 17:14:43 +00:00
obj.Put("status", "building");
2019-02-08 19:43:25 +00:00
break;
case ePathEstablished:
2019-02-11 17:14:43 +00:00
obj.Put("status", "established");
2019-02-08 19:43:25 +00:00
break;
case ePathTimeout:
2019-02-11 17:14:43 +00:00
obj.Put("status", "timeout");
2019-02-08 19:43:25 +00:00
break;
case ePathExpired:
2019-02-11 17:14:43 +00:00
obj.Put("status", "expired");
2019-02-08 19:43:25 +00:00
break;
case ePathFailed:
obj.Put("status", "failed");
break;
2019-05-31 10:57:41 +00:00
case ePathIgnore:
obj.Put("status", "ignored");
2019-07-12 06:27:21 +00:00
break;
2019-02-08 19:43:25 +00:00
default:
2019-02-11 17:14:43 +00:00
obj.Put("status", "unknown");
2019-02-08 19:43:25 +00:00
break;
}
2019-02-11 17:14:43 +00:00
return obj;
2019-02-08 19:43:25 +00:00
}
2019-05-06 14:54:05 +00:00
void
Path::Rebuild()
{
std::vector< RouterContact > newHops;
for(const auto& hop : hops)
newHops.emplace_back(hop.rc);
LogInfo(Name(), " rebuilding on ", HopsString());
m_PathSet->Build(newHops);
}
void
Path::Tick(llarp_time_t now, AbstractRouter* r)
{
if(Expired(now))
return;
if(_status == ePathBuilding)
{
if(now >= buildStarted)
{
auto dlt = now - buildStarted;
2019-04-05 14:58:22 +00:00
if(dlt >= path::build_timeout)
{
r->routerProfiling().MarkPathFail(this);
2019-04-13 14:32:07 +00:00
EnterState(ePathExpired, now);
return;
}
}
}
// check to see if this path is dead
2018-09-24 14:44:23 +00:00
if(_status == ePathEstablished)
{
2019-04-25 23:21:19 +00:00
const auto dlt = now - m_LastLatencyTestTime;
2019-04-05 14:58:22 +00:00
if(dlt > path::latency_interval && m_LastLatencyTestID == 0)
2019-02-06 13:51:05 +00:00
{
routing::PathLatencyMessage latency;
latency.T = randint();
2019-02-06 13:51:05 +00:00
m_LastLatencyTestID = latency.T;
m_LastLatencyTestTime = now;
SendRoutingMessage(latency, r);
2019-03-25 15:41:37 +00:00
return;
2019-02-06 13:51:05 +00:00
}
if(m_LastRecvMessage && now > m_LastRecvMessage)
{
2019-04-25 23:21:19 +00:00
const auto delay = now - m_LastRecvMessage;
if(m_CheckForDead && m_CheckForDead(shared_from_this(), delay))
2018-10-04 16:48:26 +00:00
{
r->routerProfiling().MarkPathFail(this);
2018-10-29 16:48:36 +00:00
EnterState(ePathTimeout, now);
2018-10-04 16:48:26 +00:00
}
}
2019-04-05 14:58:22 +00:00
else if(dlt >= path::alive_timeout && m_LastRecvMessage == 0)
{
if(m_CheckForDead && m_CheckForDead(shared_from_this(), dlt))
2019-04-10 12:56:14 +00:00
{
r->routerProfiling().MarkPathFail(this);
EnterState(ePathTimeout, now);
}
}
}
}
bool
Path::HandleUpstream(const llarp_buffer_t& buf, const TunnelNonce& Y,
AbstractRouter* r)
2018-06-20 12:34:48 +00:00
{
TunnelNonce n = Y;
for(const auto& hop : hops)
{
CryptoManager::instance()->xchacha20(buf, hop.shared, n);
n ^= hop.nonceXOR;
}
RelayUpstreamMessage msg;
msg.X = buf;
msg.Y = Y;
msg.pathid = TXID();
if(r->SendToOrQueue(Upstream(), &msg))
2018-08-10 21:34:11 +00:00
return true;
LogError("send to ", Upstream(), " failed");
2018-08-10 21:34:11 +00:00
return false;
2018-06-20 12:34:48 +00:00
}
bool
Path::Expired(llarp_time_t now) const
{
if(_status == ePathFailed)
return true;
2019-04-13 14:32:07 +00:00
if(_status == ePathEstablished || _status == ePathTimeout)
2018-12-20 15:03:48 +00:00
return now >= ExpireTime();
2019-07-06 17:03:40 +00:00
if(_status == ePathBuilding)
return false;
2019-07-06 17:03:40 +00:00
return true;
}
2018-06-23 00:00:44 +00:00
std::string
Path::Name() const
{
std::stringstream ss;
ss << "TX=" << TXID() << " RX=" << RXID();
2019-03-22 14:10:30 +00:00
if(m_PathSet)
ss << " on " << m_PathSet->Name();
return ss.str();
}
bool
Path::HandleDownstream(const llarp_buffer_t& buf, const TunnelNonce& Y,
AbstractRouter* r)
{
TunnelNonce n = Y;
for(const auto& hop : hops)
{
n ^= hop.nonceXOR;
CryptoManager::instance()->xchacha20(buf, hop.shared, n);
}
2019-03-25 15:41:37 +00:00
if(!HandleRoutingMessage(buf, r))
return false;
m_LastRecvMessage = r->Now();
return true;
}
bool
Path::HandleRoutingMessage(const llarp_buffer_t& buf, AbstractRouter* r)
{
if(!r->ParseRoutingMessageBuffer(buf, this, RXID()))
{
LogWarn("Failed to parse inbound routing message");
return false;
}
return true;
}
2018-11-14 12:23:08 +00:00
bool
Path::HandleUpdateExitVerifyMessage(
const routing::UpdateExitVerifyMessage& msg, AbstractRouter* r)
2018-11-14 12:23:08 +00:00
{
(void)r;
if(m_UpdateExitTX && msg.T == m_UpdateExitTX)
2018-11-14 12:23:08 +00:00
{
if(m_ExitUpdated)
return m_ExitUpdated(shared_from_this());
2018-11-14 12:23:08 +00:00
}
if(m_CloseExitTX && msg.T == m_CloseExitTX)
2018-11-14 12:23:08 +00:00
{
if(m_ExitClosed)
return m_ExitClosed(shared_from_this());
2018-11-14 12:23:08 +00:00
}
return false;
}
bool
Path::SendRoutingMessage(const routing::IMessage& msg, AbstractRouter* r)
{
2019-02-02 23:12:42 +00:00
std::array< byte_t, MAX_LINK_MSG_SIZE / 2 > tmp;
llarp_buffer_t buf(tmp);
// should help prevent bad paths with uninitialized members
// FIXME: Why would we get uninitialized IMessages?
if(msg.version != LLARP_PROTO_VERSION)
return false;
if(!msg.BEncode(&buf))
2018-08-10 21:34:11 +00:00
{
LogError("Bencode failed");
DumpBuffer(buf);
return false;
2018-08-10 21:34:11 +00:00
}
// make nonce
TunnelNonce N;
N.Randomize();
buf.sz = buf.cur - buf.base;
// pad smaller messages
2019-04-05 14:58:22 +00:00
if(buf.sz < pad_size)
{
// randomize padding
CryptoManager::instance()->randbytes(buf.cur, pad_size - buf.sz);
2019-04-05 14:58:22 +00:00
buf.sz = pad_size;
}
buf.cur = buf.base;
return HandleUpstream(buf, N, r);
}
2018-06-26 16:23:43 +00:00
bool
Path::HandlePathTransferMessage(
ABSL_ATTRIBUTE_UNUSED const routing::PathTransferMessage& msg,
ABSL_ATTRIBUTE_UNUSED AbstractRouter* r)
2018-06-26 16:23:43 +00:00
{
LogWarn("unwarranted path transfer message on tx=", TXID(),
" rx=", RXID());
2018-06-26 16:23:43 +00:00
return false;
}
bool
Path::HandleDataDiscardMessage(const routing::DataDiscardMessage& msg,
AbstractRouter* r)
{
2018-11-16 14:21:23 +00:00
MarkActive(r->Now());
if(m_DropHandler)
return m_DropHandler(shared_from_this(), msg.P, msg.S);
return true;
}
bool
Path::HandlePathConfirmMessage(AbstractRouter* r)
{
LogDebug("Path Build Confirm, path: ", HopsString());
2018-10-29 16:48:36 +00:00
auto now = r->Now();
if(_status == ePathBuilding)
{
// finish initializing introduction
intro.expiresAt = buildStarted + hops[0].lifetime;
r->routerProfiling().MarkPathSuccess(this);
2018-08-23 14:35:29 +00:00
// persist session with upstream router until the path is done
r->PersistSessionUntil(Upstream(), intro.expiresAt);
2018-11-16 14:21:23 +00:00
MarkActive(now);
2018-08-23 14:35:29 +00:00
// send path latency test
routing::PathLatencyMessage latency;
latency.T = randint();
2018-06-26 16:23:43 +00:00
m_LastLatencyTestID = latency.T;
2018-10-29 16:48:36 +00:00
m_LastLatencyTestTime = now;
return SendRoutingMessage(latency, r);
}
LogWarn("got unwarranted path confirm message on tx=", RXID(),
" rx=", RXID());
2018-06-22 00:25:30 +00:00
return false;
}
bool
Path::HandlePathConfirmMessage(
ABSL_ATTRIBUTE_UNUSED const routing::PathConfirmMessage& msg,
AbstractRouter* r)
{
return HandlePathConfirmMessage(r);
}
bool
Path::HandleHiddenServiceFrame(const service::ProtocolFrame& frame)
{
2018-11-16 14:21:23 +00:00
MarkActive(m_PathSet->Now());
return m_DataHandler && m_DataHandler(shared_from_this(), frame);
}
bool
Path::HandlePathLatencyMessage(const routing::PathLatencyMessage& msg,
AbstractRouter* r)
{
2018-10-29 16:48:36 +00:00
auto now = r->Now();
2018-11-16 14:21:23 +00:00
MarkActive(now);
if(msg.L == m_LastLatencyTestID)
2018-06-26 16:23:43 +00:00
{
intro.latency = now - m_LastLatencyTestTime;
m_LastLatencyTestID = 0;
EnterState(ePathEstablished, now);
if(m_BuiltHook)
m_BuiltHook(shared_from_this());
m_BuiltHook = nullptr;
2019-03-07 22:53:36 +00:00
LogDebug("path latency is now ", intro.latency, " for ", Name());
return true;
}
2019-07-06 17:03:40 +00:00
LogWarn("unwarranted path latency message via ", Upstream());
return false;
}
2018-06-19 17:11:24 +00:00
bool
Path::HandleDHTMessage(const dht::IMessage& msg, AbstractRouter* r)
{
2019-03-25 15:41:37 +00:00
MarkActive(r->Now());
routing::DHTMessage reply;
if(!msg.HandleMessage(r->dht(), reply.M))
return false;
if(reply.M.size())
return SendRoutingMessage(reply, r);
return true;
}
2018-07-09 17:32:11 +00:00
bool
Path::HandleCloseExitMessage(const routing::CloseExitMessage& msg,
ABSL_ATTRIBUTE_UNUSED AbstractRouter* r)
{
2018-11-14 18:02:27 +00:00
/// allows exits to close from their end
2018-11-21 12:31:36 +00:00
if(SupportsAnyRoles(ePathRoleExit | ePathRoleSVC))
2018-11-14 18:02:27 +00:00
{
if(msg.Verify(EndpointPubKey()))
2018-11-14 18:02:27 +00:00
{
LogInfo(Name(), " had its exit closed");
2018-11-14 18:02:27 +00:00
_role &= ~ePathRoleExit;
return true;
}
2019-07-06 17:03:40 +00:00
LogError(Name(), " CXM from exit with bad signature");
2018-11-14 18:02:27 +00:00
}
else
LogError(Name(), " unwarranted CXM");
return false;
}
2018-11-14 19:34:17 +00:00
bool
Path::SendExitRequest(const routing::ObtainExitMessage& msg,
AbstractRouter* r)
2018-11-14 19:34:17 +00:00
{
LogInfo(Name(), " sending exit request to ", Endpoint());
m_ExitObtainTX = msg.T;
2018-11-14 19:34:17 +00:00
return SendRoutingMessage(msg, r);
}
bool
Path::SendExitClose(const routing::CloseExitMessage& msg, AbstractRouter* r)
{
LogInfo(Name(), " closing exit to ", Endpoint());
// mark as not exit anymore
_role &= ~ePathRoleExit;
return SendRoutingMessage(msg, r);
}
bool
Path::HandleObtainExitMessage(const routing::ObtainExitMessage& msg,
AbstractRouter* r)
{
(void)msg;
(void)r;
LogError(Name(), " got unwarranted OXM");
return false;
}
bool
Path::HandleUpdateExitMessage(const routing::UpdateExitMessage& msg,
AbstractRouter* r)
{
(void)msg;
(void)r;
LogError(Name(), " got unwarranted UXM");
return false;
}
bool
Path::HandleRejectExitMessage(const routing::RejectExitMessage& msg,
AbstractRouter* r)
{
if(m_ExitObtainTX && msg.T == m_ExitObtainTX)
2018-11-14 18:02:27 +00:00
{
if(!msg.Verify(EndpointPubKey()))
2018-11-14 18:02:27 +00:00
{
LogError(Name(), "RXM invalid signature");
2018-11-14 18:02:27 +00:00
return false;
}
LogInfo(Name(), " ", Endpoint(), " Rejected exit");
2018-11-16 14:21:23 +00:00
MarkActive(r->Now());
return InformExitResult(msg.B);
2018-11-14 18:02:27 +00:00
}
LogError(Name(), " got unwarranted RXM");
return false;
}
bool
Path::HandleGrantExitMessage(const routing::GrantExitMessage& msg,
AbstractRouter* r)
{
if(m_ExitObtainTX && msg.T == m_ExitObtainTX)
2018-11-14 18:02:27 +00:00
{
if(!msg.Verify(EndpointPubKey()))
2018-11-14 18:02:27 +00:00
{
LogError(Name(), " GXM signature failed");
2018-11-14 18:02:27 +00:00
return false;
}
// we now can send exit traffic
_role |= ePathRoleExit;
LogInfo(Name(), " ", Endpoint(), " Granted exit");
2018-11-16 14:21:23 +00:00
MarkActive(r->Now());
2018-11-14 18:02:27 +00:00
return InformExitResult(0);
}
LogError(Name(), " got unwarranted GXM");
return false;
}
2018-11-14 18:02:27 +00:00
bool
Path::InformExitResult(llarp_time_t B)
{
2019-04-23 16:13:22 +00:00
auto self = shared_from_this();
2018-11-14 18:02:27 +00:00
bool result = true;
for(const auto& hook : m_ObtainedExitHooks)
result &= hook(self, B);
2018-11-14 18:02:27 +00:00
m_ObtainedExitHooks.clear();
return result;
}
bool
Path::HandleTransferTrafficMessage(
const routing::TransferTrafficMessage& msg, AbstractRouter* r)
{
2018-11-14 18:02:27 +00:00
// check if we can handle exit data
2018-11-21 12:31:36 +00:00
if(!SupportsAnyRoles(ePathRoleExit | ePathRoleSVC))
2018-11-14 18:02:27 +00:00
return false;
// handle traffic if we have a handler
2018-11-28 17:58:46 +00:00
if(!m_ExitTrafficHandler)
return false;
bool sent = msg.X.size() > 0;
auto self = shared_from_this();
for(const auto& pkt : msg.X)
2018-11-29 21:19:20 +00:00
{
if(pkt.size() <= 8)
return false;
uint64_t counter = bufbe64toh(pkt.data());
2019-04-22 17:55:07 +00:00
if(m_ExitTrafficHandler(
self, llarp_buffer_t(pkt.data() + 8, pkt.size() - 8), counter))
2019-04-22 17:55:07 +00:00
{
MarkActive(r->Now());
EnterState(ePathEstablished, r->Now());
}
2018-11-29 21:19:20 +00:00
}
2018-11-28 17:58:46 +00:00
return sent;
}
} // namespace path
2018-06-21 09:31:53 +00:00
} // namespace llarp