lokinet/llarp/path/pathbuilder.cpp

579 lines
16 KiB
C++
Raw Normal View History

#include "pathbuilder.hpp"
#include "path.hpp"
#include "path_context.hpp"
#include <llarp/crypto/crypto.hpp>
2023-10-04 13:25:25 +00:00
#include <llarp/link/link_manager.hpp>
#include <llarp/messages/path.hpp>
#include <llarp/nodedb.hpp>
#include <llarp/profiling.hpp>
#include <llarp/router/rc_lookup_handler.hpp>
#include <llarp/router/router.hpp>
2023-10-04 13:25:25 +00:00
#include <llarp/util/logging.hpp>
2018-06-18 22:03:50 +00:00
#include <functional>
2018-06-18 22:03:50 +00:00
namespace llarp
{
namespace
{
auto path_cat = log::Cat("path");
}
namespace path
2018-06-21 12:55:02 +00:00
{
bool
BuildLimiter::Attempt(const RouterID& router)
{
return m_EdgeLimiter.Insert(router);
}
2018-06-21 12:55:02 +00:00
2019-06-20 16:22:29 +00:00
void
BuildLimiter::Decay(llarp_time_t now)
2018-06-21 12:55:02 +00:00
{
m_EdgeLimiter.Decay(now);
}
bool
BuildLimiter::Limited(const RouterID& router) const
{
return m_EdgeLimiter.Contains(router);
}
Builder::Builder(Router* p_router, size_t pathNum, size_t hops)
: path::PathSet{pathNum}, _run{true}, router{p_router}, numHops{hops}
{}
/* - For each hop:
* SetupHopKeys:
* - Generate Ed keypair for the hop. ("commkey")
* - Use that key and the hop's pubkey for DH key exchange (makes "hop.shared")
* - Note: this *was* using hop's "enckey" but we're getting rid of that
* - hop's "upstream" RouterID is next hop, or that hop's ID if it is terminal hop
* - hop's chacha nonce is hash of symmetric key (hop.shared) from DH
* - hop's "txID" and "rxID" are chosen before this step
* - txID is the path ID for messages coming *from* the client/path origin
* - rxID is the path ID for messages going *to* it.
*
* CreateHopInfoFrame:
* - bt-encode "hop info":
* - path lifetime
* - protocol version
* - txID
* - rxID
* - nonce
* - upstream hop RouterID
* - ephemeral public key (for DH)
* - generate *second* ephemeral Ed keypair... ("framekey") TODO: why?
* - generate DH symmetric key using "framekey" and hop's pubkey
* - generate nonce for second encryption
* - encrypt "hop info" using this symmetric key
* - bt-encode nonce, "framekey" pubkey, encrypted "hop info"
* - hash this bt-encoded string
* - bt-encode hash and the frame in a dict, serialize
*
*
* all of these "frames" go in a list, along with any needed dummy frames
*/
void
2023-10-04 13:25:25 +00:00
Builder::setup_hop_keys(path::PathHopConfig& hop, const RouterID& nextHop)
{
2018-06-21 12:55:02 +00:00
// generate key
crypto::encryption_keygen(hop.commkey);
2018-06-21 12:55:02 +00:00
hop.nonce.Randomize();
// do key exchange
if (!crypto::dh_client(hop.shared, hop.rc.router_id(), hop.commkey, hop.nonce))
2018-06-21 12:55:02 +00:00
{
2023-10-04 13:25:25 +00:00
auto err = fmt::format("{} failed to generate shared key for path build!", Name());
log::error(path_cat, err);
throw std::runtime_error{std::move(err)};
2018-06-21 12:55:02 +00:00
}
// generate nonceXOR value self->hop->pathKey
crypto::shorthash(hop.nonceXOR, hop.shared.data(), hop.shared.size());
2018-06-21 14:20:14 +00:00
hop.upstream = nextHop;
}
2018-06-21 14:20:14 +00:00
std::string
2023-10-04 13:25:25 +00:00
Builder::create_hop_info_frame(const path::PathHopConfig& hop)
{
2023-10-04 13:25:25 +00:00
std::string hop_info;
{
oxenc::bt_dict_producer btdp;
2023-10-16 16:55:51 +00:00
btdp.append("COMMKEY", hop.commkey.toPublic().ToView());
btdp.append("LIFETIME", path::DEFAULT_LIFETIME.count());
btdp.append("NONCE", hop.nonce.ToView());
btdp.append("RX", hop.rxID.ToView());
btdp.append("TX", hop.txID.ToView());
btdp.append("UPSTREAM", hop.upstream.ToView());
2023-10-04 13:25:25 +00:00
hop_info = std::move(btdp).str();
}
2018-06-21 12:55:02 +00:00
SecretKey framekey;
crypto::encryption_keygen(framekey);
2018-06-21 12:55:02 +00:00
SharedSecret shared;
TunnelNonce outer_nonce;
outer_nonce.Randomize();
2023-10-04 13:25:25 +00:00
// derive (outer) shared key
if (!crypto::dh_client(shared, hop.rc.router_id(), framekey, outer_nonce))
2018-06-21 12:55:02 +00:00
{
2023-10-04 13:25:25 +00:00
log::error(path_cat, "DH client failed during hop info encryption!");
throw std::runtime_error{"DH failed during hop info encryption"};
2018-06-21 12:55:02 +00:00
}
// encrypt hop_info (mutates in-place)
if (!crypto::xchacha20(
reinterpret_cast<uint8_t*>(hop_info.data()), hop_info.size(), shared, outer_nonce))
2018-06-21 12:55:02 +00:00
{
2023-10-04 13:25:25 +00:00
log::error(path_cat, "Hop info encryption failed!");
throw std::runtime_error{"Hop info encrypttion failed"};
2018-06-21 12:55:02 +00:00
}
2023-10-04 13:25:25 +00:00
std::string hashed_data;
{
oxenc::bt_dict_producer btdp;
2023-10-16 16:55:51 +00:00
btdp.append("ENCRYPTED", hop_info);
btdp.append("NONCE", outer_nonce.ToView());
btdp.append("PUBKEY", framekey.toPublic().ToView());
2023-10-04 13:25:25 +00:00
hashed_data = std::move(btdp).str();
}
std::string hash;
hash.reserve(SHORTHASHSIZE);
if (!crypto::hmac(
2023-10-04 13:25:25 +00:00
reinterpret_cast<uint8_t*>(hash.data()),
reinterpret_cast<uint8_t*>(hashed_data.data()),
hashed_data.size(),
shared))
{
2023-10-04 13:25:25 +00:00
log::error(path_cat, "Failed to generate HMAC for hop info");
throw std::runtime_error{"Failed to generate HMAC for hop info"};
}
2021-05-05 12:21:39 +00:00
2023-10-04 13:25:25 +00:00
oxenc::bt_dict_producer btdp;
2021-05-05 12:21:39 +00:00
2023-10-16 16:55:51 +00:00
btdp.append("FRAME", hashed_data);
btdp.append("HASH", hash);
2023-10-04 13:25:25 +00:00
return std::move(btdp).str();
2018-08-30 18:48:43 +00:00
}
2019-05-07 17:46:38 +00:00
void
Builder::ResetInternalState()
{
buildIntervalLimit = PATH_BUILD_RATE;
lastBuild = 0s;
2019-05-07 17:46:38 +00:00
}
void
Builder::Tick(llarp_time_t now)
2019-04-23 16:13:22 +00:00
{
PathSet::Tick(now);
now = llarp::time_now_ms();
2023-10-03 20:00:23 +00:00
router->pathbuild_limiter().Decay(now);
2023-10-03 20:00:23 +00:00
ExpirePaths(now, router);
if (ShouldBuildMore(now))
2019-04-23 16:13:22 +00:00
BuildOne();
2023-10-03 20:00:23 +00:00
TickPaths(router);
if (m_BuildStats.attempts > 50)
2019-07-01 13:44:25 +00:00
{
if (m_BuildStats.SuccessRatio() <= BuildStats::MinGoodRatio && now - m_LastWarn > 5s)
{
LogWarn(Name(), " has a low path build success. ", m_BuildStats);
m_LastWarn = now;
}
2019-07-01 13:44:25 +00:00
}
2019-04-23 16:13:22 +00:00
}
2019-02-11 17:14:43 +00:00
util::StatusObject
Builder::ExtractStatus() const
2019-02-08 19:43:25 +00:00
{
2021-03-05 17:31:52 +00:00
util::StatusObject obj{
{"buildStats", m_BuildStats.ExtractStatus()},
{"numHops", uint64_t{numHops}},
{"numPaths", uint64_t{numDesiredPaths}}};
std::transform(
m_Paths.begin(),
m_Paths.end(),
std::back_inserter(obj["paths"]),
[](const auto& item) -> util::StatusObject { return item.second->ExtractStatus(); });
2019-02-11 17:14:43 +00:00
return obj;
2019-02-08 19:43:25 +00:00
}
std::optional<RemoteRC>
Builder::SelectFirstHop(const std::set<RouterID>& exclude) const
2018-08-30 18:48:43 +00:00
{
std::optional<RemoteRC> found = std::nullopt;
2023-10-03 20:00:23 +00:00
router->for_each_connection([&](link::Connection& conn) {
const auto& rc = conn.remote_rc;
const auto& rid = rc.router_id();
#ifndef TESTNET
if (router->IsBootstrapNode(rid))
return;
2019-09-05 20:20:55 +00:00
#endif
if (exclude.count(rid))
return;
if (BuildCooldownHit(rid))
return;
if (router->router_profiling().IsBadForPath(rid))
return;
found = rc;
});
return found;
}
2019-06-20 16:22:29 +00:00
std::optional<std::vector<RemoteRC>>
Builder::GetHopsForBuild()
{
2023-10-03 20:00:23 +00:00
auto filter = [r = router](const auto& rc) -> bool {
return not r->router_profiling().IsBadForPath(rc.router_id(), 1);
};
2023-10-03 20:00:23 +00:00
if (const auto maybe = router->node_db()->GetRandom(filter))
2018-10-04 17:51:45 +00:00
{
return GetHopsAlignedToForBuild(maybe->router_id());
}
return std::nullopt;
2018-08-30 18:48:43 +00:00
}
bool
Builder::Stop()
{
2019-06-20 16:22:29 +00:00
_run = false;
// tell all our paths that they are to be ignored
const auto now = Now();
for (auto& item : m_Paths)
{
item.second->EnterState(ePathIgnore, now);
}
return true;
}
2019-02-05 14:50:33 +00:00
bool
Builder::IsStopped() const
{
return !_run.load();
}
bool
Builder::ShouldRemove() const
{
return IsStopped() and NumInStatus(ePathEstablished) == 0;
}
bool
Builder::BuildCooldownHit(RouterID edge) const
{
2023-10-03 20:00:23 +00:00
return router->pathbuild_limiter().Limited(edge);
}
2018-12-27 12:00:28 +00:00
bool
Builder::BuildCooldownHit(llarp_time_t now) const
{
2020-03-01 15:58:08 +00:00
return now < lastBuild + buildIntervalLimit;
2018-12-27 12:00:28 +00:00
}
2018-08-30 18:48:43 +00:00
bool
2018-10-29 16:48:36 +00:00
Builder::ShouldBuildMore(llarp_time_t now) const
2018-08-30 18:48:43 +00:00
{
if (IsStopped())
2019-04-16 11:44:55 +00:00
return false;
if (BuildCooldownHit(now))
2019-11-05 16:58:53 +00:00
return false;
return PathSet::ShouldBuildMore(now);
2018-08-30 18:48:43 +00:00
}
2018-08-12 17:22:29 +00:00
2018-08-30 18:48:43 +00:00
void
2018-11-14 18:02:27 +00:00
Builder::BuildOne(PathRole roles)
2018-08-30 18:48:43 +00:00
{
2021-04-12 11:39:07 +00:00
if (const auto maybe = GetHopsForBuild())
Build(*maybe, roles);
}
2022-10-20 22:23:14 +00:00
bool
Builder::UrgentBuild(llarp_time_t) const
{
return buildIntervalLimit > MIN_PATH_BUILD_INTERVAL * 4;
}
std::optional<std::vector<RemoteRC>>
Builder::GetHopsAlignedToForBuild(RouterID endpoint, const std::set<RouterID>& exclude)
{
2023-10-03 20:00:23 +00:00
const auto pathConfig = router->config()->paths;
std::vector<RemoteRC> hops;
2019-06-20 16:22:29 +00:00
{
const auto maybe = SelectFirstHop(exclude);
if (not maybe.has_value())
2021-04-12 11:39:07 +00:00
{
log::warning(path_cat, "{} has no first hop candidate", Name());
return std::nullopt;
2021-04-12 11:39:07 +00:00
}
hops.emplace_back(*maybe);
};
RemoteRC endpointRC;
if (const auto maybe = router->node_db()->get_rc(endpoint))
{
endpointRC = *maybe;
}
else
return std::nullopt;
for (size_t idx = hops.size(); idx < numHops; ++idx)
{
if (idx + 1 == numHops)
{
hops.emplace_back(endpointRC);
}
2019-06-20 16:22:29 +00:00
else
{
auto filter =
[&hops, r = router, endpointRC, pathConfig, exclude](const RemoteRC& rc) -> bool {
const auto& rid = rc.router_id();
if (exclude.count(rid))
return false;
std::set<RemoteRC> hopsSet;
hopsSet.insert(endpointRC);
hopsSet.insert(hops.begin(), hops.end());
if (r->router_profiling().IsBadForPath(rid, 1))
return false;
for (const auto& hop : hopsSet)
{
if (hop.router_id() == rid)
return false;
}
hopsSet.insert(rc);
2021-04-12 11:38:12 +00:00
#ifndef TESTNET
2023-11-03 13:40:14 +00:00
if (not pathConfig.check_rcs(hopsSet))
return false;
2021-04-12 11:38:12 +00:00
#endif
return rc.router_id() != endpointRC.router_id();
};
2023-10-03 20:00:23 +00:00
if (const auto maybe = router->node_db()->GetRandom(filter))
hops.emplace_back(*maybe);
else
return std::nullopt;
}
}
return hops;
2019-06-20 16:22:29 +00:00
}
bool
Builder::BuildOneAlignedTo(const RouterID remote)
{
if (const auto maybe = GetHopsAlignedToForBuild(remote); maybe.has_value())
{
LogInfo(Name(), " building path to ", remote);
Build(*maybe);
return true;
2019-06-20 16:22:29 +00:00
}
return false;
}
2018-10-29 16:48:36 +00:00
llarp_time_t
Builder::Now() const
{
2023-10-03 20:00:23 +00:00
return router->now();
2018-10-29 16:48:36 +00:00
}
void
Builder::Build(std::vector<RemoteRC> hops, PathRole roles)
{
if (IsStopped())
2023-10-04 13:25:25 +00:00
{
log::info(path_cat, "Path builder is stopped, aborting path build...");
return;
2023-10-04 13:25:25 +00:00
}
lastBuild = llarp::time_now_ms();
const auto& edge = hops[0].router_id();
2023-10-04 13:25:25 +00:00
2023-10-03 20:00:23 +00:00
if (not router->pathbuild_limiter().Attempt(edge))
{
2023-10-04 13:25:25 +00:00
log::warning(path_cat, "{} building too quickly to edge router {}", Name(), edge);
return;
}
2023-10-03 20:00:23 +00:00
std::string path_shortName = "[path " + router->ShortName() + "-";
path_shortName = path_shortName + std::to_string(router->NextPathBuildNumber()) + "]";
auto path =
std::make_shared<path::Path>(router, hops, GetWeak(), roles, std::move(path_shortName));
2023-10-04 13:25:25 +00:00
log::info(
path_cat, "{} building path -> {} : {}", Name(), path->ShortName(), path->HopsString());
oxenc::bt_list_producer frames;
2023-10-04 13:25:25 +00:00
auto& path_hops = path->hops;
2023-10-04 13:25:25 +00:00
size_t n_hops = path_hops.size();
size_t last_len{0};
2023-10-04 13:25:25 +00:00
for (size_t i = 0; i < n_hops; i++)
{
2023-10-04 13:25:25 +00:00
bool lastHop = (i == (n_hops - 1));
const auto& nextHop =
lastHop ? path_hops[i].rc.router_id() : path_hops[i + 1].rc.router_id();
2023-10-04 13:25:25 +00:00
PathBuildMessage::setup_hop_keys(path_hops[i], nextHop);
auto frame_str = PathBuildMessage::serialize(path_hops[i]);
// all frames should be the same length...not sure what that is yet
if (last_len != 0)
2023-10-04 13:25:25 +00:00
assert(frame_str.size() == last_len);
last_len = frame_str.size();
2023-10-04 13:25:25 +00:00
frames.append(std::move(frame_str));
}
2023-10-04 13:25:25 +00:00
std::string dummy;
dummy.reserve(last_len);
// append dummy frames; path build request must always have MAX_LEN frames
// TODO: with the data structured as it is now (bt-encoded dict as each frame)
// the dummy frames can't be completely random; they need to look like
// normal frames
2023-10-04 13:25:25 +00:00
for (size_t i = 0; i < path::MAX_LEN - n_hops; i++)
{
randombytes(reinterpret_cast<uint8_t*>(dummy.data()), dummy.size());
frames.append(dummy);
}
auto self = GetSelf();
2023-10-04 13:25:25 +00:00
router->path_context().AddOwnPath(self, path);
PathBuildStarted(path);
// TODO:
// Path build fail and success are handled poorly at best and changing how we
// handle these responses as well as how we store and use Paths as a whole might
// be worth doing sooner rather than later. Leaving some TODOs below where fail
// and success live.
auto response_cb = [self](oxen::quic::message m) {
try
{
if (not m)
{
if (m.timed_out)
{
log::warning(path_cat, "Path build timed out");
}
else
{
oxenc::bt_dict_consumer d{m.body()};
auto status = d.require<std::string_view>("STATUS");
log::warning(path_cat, "Path build returned failure status: {}", status);
}
// TODO: inform failure (what this means needs revisiting, badly)
return;
}
// TODO: inform success (what this means needs revisiting, badly)
}
catch (const std::exception& e)
{
log::warning(path_cat, "Failed parsing path build response.");
// TODO: inform failure (what this means needs revisiting, badly)
}
};
2023-10-04 13:25:25 +00:00
if (not router->send_control_message(
path->upstream(), "path_build", std::move(frames).str(), std::move(response_cb)))
{
log::warning(path_cat, "Error sending path_build control message");
// TODO: inform failure (what this means needs revisiting, badly)
2023-10-04 13:25:25 +00:00
path->EnterState(path::ePathFailed, router->now());
}
2018-08-30 18:48:43 +00:00
}
2018-06-18 22:03:50 +00:00
void
Builder::HandlePathBuilt(Path_ptr p)
{
buildIntervalLimit = PATH_BUILD_RATE;
2023-10-03 20:00:23 +00:00
router->router_profiling().MarkPathSuccess(p.get());
2023-10-04 13:25:25 +00:00
LogInfo(p->name(), " built latency=", ToString(p->intro.latency));
2019-07-01 13:44:25 +00:00
m_BuildStats.success++;
}
void
Builder::HandlePathBuildFailedAt(Path_ptr p, RouterID edge)
{
PathSet::HandlePathBuildFailedAt(p, edge);
DoPathBuildBackoff();
}
void
Builder::DoPathBuildBackoff()
{
static constexpr std::chrono::milliseconds MaxBuildInterval = 30s;
// linear backoff
buildIntervalLimit = std::min(PATH_BUILD_RATE + buildIntervalLimit, MaxBuildInterval);
LogWarn(Name(), " build interval is now ", ToString(buildIntervalLimit));
}
void
Builder::HandlePathBuildTimeout(Path_ptr p)
{
2023-10-03 20:00:23 +00:00
router->router_profiling().MarkPathTimeout(p.get());
PathSet::HandlePathBuildTimeout(p);
DoPathBuildBackoff();
for (const auto& hop : p->hops)
{
const auto& target = hop.rc.router_id();
// look up router and see if it's still on the network
log::info(path_cat, "Looking up RouterID {} due to path build timeout", target);
router->rc_lookup_handler().get_rc(
target,
[this](auto rid, auto rc, auto success) {
if (success && rc)
{
log::info(path_cat, "Refreshed RouterContact for {}", rid);
router->node_db()->put_rc_if_newer(*rc);
}
else
{
// remove all connections to this router as it's probably not registered anymore
log::warning(path_cat, "Removing router {} due to path build timeout", rid);
router->link_manager().deregister_peer(rid);
router->node_db()->remove_router(rid);
}
},
true);
}
}
2018-08-30 18:48:43 +00:00
void
2018-11-14 18:02:27 +00:00
Builder::ManualRebuild(size_t num, PathRole roles)
2018-08-30 18:48:43 +00:00
{
2019-03-22 14:10:30 +00:00
LogDebug(Name(), " manual rebuild ", num);
while (num--)
2018-11-14 18:02:27 +00:00
BuildOne(roles);
2018-08-30 18:48:43 +00:00
}
2018-06-18 22:03:50 +00:00
2018-08-30 18:48:43 +00:00
} // namespace path
} // namespace llarp