#include "endpoint.hpp" #include "auth.hpp" #include "endpoint_state.hpp" #include "endpoint_util.hpp" #include "info.hpp" #include "outbound_context.hpp" #include "protocol.hpp" #include "protocol_type.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace llarp::service { static auto logcat = log::Cat("endpoint"); Endpoint::Endpoint(Router* r, Context* parent) : path::Builder{r, 3, path::DEFAULT_LEN} , context{parent} , _inbound_queue{512} , _send_queue{512} , _recv_event_queue{512} , _introset_lookup_filter{5s} { _state = std::make_unique(); _state->router = r; _state->name = "endpoint"; _recv_event_queue.enable(); // if (Loop()->MaybeGetUVWLoop()) // _tunnel_manager = std::make_unique(*this); } bool Endpoint::Configure(const NetworkConfig& conf, [[maybe_unused]] const DnsConfig& dnsConf) { if (conf.m_Paths.has_value()) numDesiredPaths = *conf.m_Paths; if (conf.m_Hops.has_value()) numHops = *conf.m_Hops; conf.m_ExitMap.ForEachEntry( [&](const IPRange& range, const service::Address& addr) { MapExitRange(range, addr); }); for (auto [exit, auth] : conf.m_ExitAuths) { SetAuthInfoForEndpoint(exit, auth); } conf.m_LNSExitMap.ForEachEntry([&](const IPRange& range, const std::string& name) { std::optional auth; const auto itr = conf.m_LNSExitAuths.find(name); if (itr != conf.m_LNSExitAuths.end()) auth = itr->second; _startup_ons_mappings[name] = std::make_pair(range, auth); }); return _state->Configure(conf); } bool Endpoint::HasPendingPathToService(const Address& addr) const { return _state->pending_service_lookups.find(addr) != _state->pending_service_lookups.end(); } bool Endpoint::is_ready() const { const auto now = llarp::time_now_ms(); if (intro_set().intros.empty()) return false; if (intro_set().IsExpired(now)) return false; return true; } bool Endpoint::HasPendingRouterLookup(const RouterID remote) const { const auto& routers = _state->pending_routers; return routers.find(remote) != routers.end(); } std::optional> Endpoint::GetEndpointWithConvoTag(ConvoTag tag) const { auto itr = Sessions().find(tag); if (itr != Sessions().end()) { return itr->second.remote.Addr(); } for (const auto& item : _state->snode_sessions) { if (const auto maybe = item.second->CurrentPath()) { if (ConvoTag{maybe->as_array()} == tag) return item.first; } } return std::nullopt; } void Endpoint::map_exit( std::string name, std::string token, std::vector ranges, std::function result_handler) { if (ranges.empty()) { result_handler(false, "no ranges provided"); return; } lookup_name( name, [ptr = std::static_pointer_cast(GetSelf()), name, auth = AuthInfo{token}, ranges, result_handler, poker = router()->route_poker()](oxen::quic::message m) mutable { if (m) { std::string name; try { oxenc::bt_dict_consumer btdc{m.body()}; name = btdc.require("NAME"); } catch (...) { log::warning(link_cat, "Failed to parse find name response!"); throw; } if (auto saddr = service::Address(); saddr.FromString(name)) { ptr->SetAuthInfoForEndpoint(saddr, auth); ptr->MarkAddressOutbound(saddr); auto result = ptr->EnsurePathToService( saddr, [ptr, name, ranges, result_handler, poker](auto addr, OutboundContext* ctx) { if (ctx == nullptr) { result_handler(false, "could not establish flow to {}"_format(name)); return; } // make a lambda that sends the reply after doing auth auto apply_result = [ptr, poker, addr, result_handler, ranges]( std::string result, bool success) { if (success) { for (const auto& range : ranges) ptr->MapExitRange(range, addr); if (poker) poker->put_up(); result_handler(true, result); } result_handler(false, result); }; ctx->send_auth_async(apply_result); }, ptr->PathAlignmentTimeout()); if (not result) result_handler(false, "Could not build path to {}"_format(name)); } } else { result_handler(false, "Exit {} not found!"_format(name)); } }); } void Endpoint::LookupServiceAsync( std::string name, std::string service, std::function)> resultHandler) { // handles when we aligned to a loki address auto handleGotPathToService = [resultHandler, service, this](auto addr) { // we can probably get this info before we have a path to them but we do this after we // have a path so when we send the response back they can send shit to them immediately const auto& container = _state->remote_sessions; if (auto itr = container.find(addr); itr != container.end()) { // parse the stuff we need from this guy resultHandler(itr->second->GetCurrentIntroSet().GetMatchingSRVRecords(service)); return; } resultHandler({}); }; // handles when we resolved a .snode auto handleResolvedSNodeName = [resultHandler, nodedb = router()->node_db()](auto router_id) { std::vector result{}; if (auto maybe_rc = nodedb->get_rc(router_id)) { result = maybe_rc->srvRecords; // TODO: RouterContact has no SRV records } resultHandler(std::move(result)); }; // handles when we got a path to a remote thing auto handleGotPathTo = [handleGotPathToService, handleResolvedSNodeName, resultHandler]( auto maybe_tag, auto address) { if (not maybe_tag) { resultHandler({}); return; } if (auto* addr = std::get_if
(&address)) { // .loki case handleGotPathToService(*addr); } else if (auto* router_id = std::get_if(&address)) { // .snode case handleResolvedSNodeName(*router_id); } else { // fallback case // XXX: never should happen but we'll handle it anyways resultHandler({}); } }; // handles when we know a long address of a remote resource auto handleGotAddress = [resultHandler, handleGotPathTo, this](AddressVariant_t address) { // we will attempt a build to whatever we looked up const auto result = EnsurePathTo( address, [address, handleGotPathTo](auto maybe_tag) { handleGotPathTo(maybe_tag, address); }, PathAlignmentTimeout()); // on path build start fail short circuit if (not result) resultHandler({}); }; // look up this name async and start the entire chain of events lookup_name(name, [handleGotAddress, resultHandler](oxen::quic::message m) mutable { if (m) { std::string name; try { oxenc::bt_dict_consumer btdc{m.body()}; name = btdc.require("NAME"); } catch (...) { log::warning(link_cat, "Failed to parse find name response!"); throw; } if (auto saddr = service::Address(); saddr.FromString(name)) handleGotAddress(saddr); if (auto rid = RouterID(); rid.FromString(name)) handleGotAddress(rid); } else { resultHandler({}); } }); } bool Endpoint::IntrosetIsStale() const { return intro_set().HasExpiredIntros(llarp::time_now_ms()); } util::StatusObject Endpoint::ExtractStatus() const { auto obj = path::Builder::ExtractStatus(); obj["exitMap"] = _exit_map.ExtractStatus(); obj["identity"] = _identity.pub.Addr().ToString(); obj["networkReady"] = ReadyForNetwork(); util::StatusObject authCodes; for (const auto& [service, info] : _remote_auth_infos) { authCodes[service.ToString()] = info.token; } obj["authCodes"] = authCodes; return _state->ExtractStatus(obj); } void Endpoint::Tick(llarp_time_t) { const auto now = llarp::time_now_ms(); path::Builder::Tick(now); // publish descriptors if (ShouldPublishDescriptors(now)) { regen_and_publish_introset(); } // decay introset lookup filter _introset_lookup_filter.Decay(now); // expire name cache _state->nameCache.Decay(now); // expire snode sessions EndpointUtil::ExpireSNodeSessions(now, _state->snode_sessions); // expire pending router lookups EndpointUtil::ExpirePendingRouterLookups(now, _state->pending_routers); // deregister dead sessions EndpointUtil::DeregisterDeadSessions(now, _state->dead_sessions); // tick remote sessions EndpointUtil::TickRemoteSessions( now, _state->remote_sessions, _state->dead_sessions, Sessions()); // expire convotags EndpointUtil::ExpireConvoSessions(now, Sessions()); if (NumInStatus(path::ePathEstablished) > 1) { for (const auto& item : _startup_ons_mappings) { auto& name = item.first; lookup_name(name, [this, name, info = item.second](oxen::quic::message m) mutable { if (m) { std::string result; try { oxenc::bt_dict_consumer btdc{m.body()}; result = btdc.require("NAME"); } catch (...) { log::warning(link_cat, "Failed to parse find name response!"); throw; } const auto maybe_range = info.first; const auto maybe_auth = info.second; _startup_ons_mappings.erase(name); if (auto saddr = service::Address(); saddr.FromString(result)) { if (maybe_range.has_value()) _exit_map.Insert(*maybe_range, saddr); if (maybe_auth.has_value()) SetAuthInfoForEndpoint(saddr, *maybe_auth); } } }); } } } bool Endpoint::Stop() { // stop remote sessions log::debug(logcat, "Endpoint stopping remote sessions."); EndpointUtil::StopRemoteSessions(_state->remote_sessions); // stop snode sessions log::debug(logcat, "Endpoint stopping snode sessions."); EndpointUtil::StopSnodeSessions(_state->snode_sessions); log::debug(logcat, "Endpoint stopping its path builder."); return path::Builder::Stop(); } uint64_t Endpoint::GenTXID() { return randint(); } std::string Endpoint::Name() const { return _state->name + ":" + _identity.pub.Name(); } bool Endpoint::HasInboundConvo(const Address& addr) const { for (const auto& item : Sessions()) { if (item.second.remote.Addr() == addr and item.second.inbound) return true; } return false; } bool Endpoint::HasOutboundConvo(const Address& addr) const { for (const auto& item : Sessions()) { if (item.second.remote.Addr() == addr && not item.second.inbound) return true; } return false; } void Endpoint::PutSenderFor(const ConvoTag& tag, const ServiceInfo& info, bool inbound) { if (info.Addr().IsZero()) { LogError(Name(), " cannot put invalid service info ", info, " T=", tag); return; } auto itr = Sessions().find(tag); if (itr == Sessions().end()) { if (WantsOutboundSession(info.Addr()) and inbound) { LogWarn( Name(), " not adding sender for ", info.Addr(), " session is inbound and we want outbound T=", tag); return; } itr = Sessions().emplace(tag, Session{}).first; itr->second.inbound = inbound; itr->second.remote = info; } } size_t Endpoint::RemoveAllConvoTagsFor(service::Address remote) { size_t removed = 0; auto& sessions = Sessions(); auto itr = sessions.begin(); while (itr != sessions.end()) { if (itr->second.remote.Addr() == remote) { itr = sessions.erase(itr); removed++; } else ++itr; } return removed; } bool Endpoint::GetSenderFor(const ConvoTag& tag, ServiceInfo& si) const { auto itr = Sessions().find(tag); if (itr == Sessions().end()) return false; si = itr->second.remote; si.UpdateAddr(); return true; } void Endpoint::PutIntroFor(const ConvoTag& tag, const Introduction& intro) { auto& s = Sessions()[tag]; s.intro = intro; } bool Endpoint::GetIntroFor(const ConvoTag& tag, Introduction& intro) const { auto itr = Sessions().find(tag); if (itr == Sessions().end()) return false; intro = itr->second.intro; return true; } void Endpoint::PutReplyIntroFor(const ConvoTag& tag, const Introduction& intro) { auto itr = Sessions().find(tag); if (itr == Sessions().end()) { return; } itr->second.replyIntro = intro; } bool Endpoint::GetReplyIntroFor(const ConvoTag& tag, Introduction& intro) const { auto itr = Sessions().find(tag); if (itr == Sessions().end()) return false; intro = itr->second.replyIntro; return true; } bool Endpoint::GetConvoTagsForService(const Address& addr, std::set& tags) const { return EndpointUtil::GetConvoTagsForService(Sessions(), addr, tags); } bool Endpoint::GetCachedSessionKeyFor(const ConvoTag& tag, SharedSecret& secret) const { auto itr = Sessions().find(tag); if (itr == Sessions().end()) return false; secret = itr->second.sharedKey; return true; } void Endpoint::PutCachedSessionKeyFor(const ConvoTag& tag, const SharedSecret& k) { auto itr = Sessions().find(tag); if (itr == Sessions().end()) { itr = Sessions().emplace(tag, Session{}).first; } itr->second.sharedKey = k; } void Endpoint::ConvoTagTX(const ConvoTag& tag) { if (Sessions().count(tag)) Sessions()[tag].TX(); } void Endpoint::ConvoTagRX(const ConvoTag& tag) { if (Sessions().count(tag)) Sessions()[tag].RX(); } bool Endpoint::LoadKeyFile() { const auto& keyfile = _state->key_file; if (!keyfile.empty()) { _identity.EnsureKeys(keyfile, router()->key_manager()->needBackup()); } else { _identity.RegenerateKeys(); } return true; } bool Endpoint::Start() { // this does network isolation while (_state->on_init_callbacks.size()) { if (_state->on_init_callbacks.front()()) _state->on_init_callbacks.pop_front(); else { LogWarn("Can't call init of network isolation"); return false; } } return true; } // Keep this here (rather than the header) so that we don't need to include endpoint_state.hpp // in endpoint.hpp for the unique_ptr member destructor. Endpoint::~Endpoint() = default; void Endpoint::regen_and_publish_introset() { const auto now = llarp::time_now_ms(); _last_introset_regen_attempt = now; std::set intros; if (const auto maybe = GetCurrentIntroductionsWithFilter([now](const service::Introduction& intro) -> bool { return not intro.ExpiresSoon(now, path::INTRO_STALE_THRESHOLD); })) { intros.insert(maybe->begin(), maybe->end()); } else { LogWarn( "could not publish descriptors for endpoint ", Name(), " because we couldn't get enough valid introductions"); BuildOne(); return; } intro_set().supported_protocols.clear(); // add supported ethertypes if (HasIfAddr()) { if (IPRange::V4MappedRange().Contains(GetIfAddr())) { intro_set().supported_protocols.push_back(ProtocolType::TrafficV4); } else { intro_set().supported_protocols.push_back(ProtocolType::TrafficV6); } // exit related stuffo if (_state->is_exit_enabled) { intro_set().supported_protocols.push_back(ProtocolType::Exit); intro_set().exit_policy = GetExitPolicy(); intro_set().owned_ranges = GetOwnedRanges(); } } // add quic ethertype if we have listeners set up // if (auto* quic = GetQUICTunnel()) // { // TODO: // if (quic->hasListeners()) // intro_set().supported_protocols.push_back(ProtocolType::QUIC); // } intro_set().intros.clear(); for (auto& intro : intros) { if (intro_set().intros.size() < numDesiredPaths) intro_set().intros.emplace_back(std::move(intro)); } if (intro_set().intros.empty()) { LogWarn("not enough intros to publish introset for ", Name()); if (ShouldBuildMore(now)) ManualRebuild(1); return; } auto maybe = _identity.encrypt_and_sign_introset(intro_set(), now); if (not maybe) { LogWarn("failed to generate introset for endpoint ", Name()); return; } if (publish_introset(*maybe)) { LogInfo("(re)publishing introset for endpoint ", Name()); } else { LogWarn("failed to publish intro set for endpoint ", Name()); } } bool Endpoint::publish_introset(const EncryptedIntroSet& introset) { const auto paths = GetManyPathsWithUniqueEndpoints( this, INTROSET_RELAY_REDUNDANCY, dht::Key_t{introset.derivedSigningKey.as_array()}); if (paths.size() != INTROSET_RELAY_REDUNDANCY) { LogWarn( "Cannot publish intro set because we only have ", paths.size(), " paths, but need ", INTROSET_RELAY_REDUNDANCY); return false; } for (const auto& path : paths) { for (size_t i = 0; i < INTROSET_REQS_PER_RELAY; ++i) { router()->send_control_message(path->upstream(), "publish_intro", introset.bt_encode()); } } return true; } size_t Endpoint::UniqueEndpoints() const { return _state->remote_sessions.size() + _state->snode_sessions.size(); } [[maybe_unused]] constexpr auto PublishIntrosetTimeout = 20s; void Endpoint::ResetInternalState() { path::Builder::ResetInternalState(); static auto resetState = [](auto& container, auto getter) { std::for_each(container.begin(), container.end(), [getter](auto& item) { getter(item)->ResetInternalState(); }); }; resetState(_state->remote_sessions, [](const auto& item) { return item.second; }); resetState(_state->snode_sessions, [](const auto& item) { return item.second; }); } bool Endpoint::ShouldPublishDescriptors(llarp_time_t now) const { if (not _publish_introset) return false; const auto lastEventAt = std::max(_state->last_publish_attempt, _state->last_publish); const auto next_pub = lastEventAt + (_state->local_introset.HasStaleIntros(now, path::INTRO_STALE_THRESHOLD) ? IntrosetPublishRetryCooldown : IntrosetPublishInterval); return now >= next_pub; } std::optional> Endpoint::GetHopsForBuild() { std::unordered_set exclude; ForEachPath([&exclude](auto path) { exclude.insert(path->Endpoint()); }); const auto maybe = router()->node_db()->GetRandom([exclude, r = router()](const auto& rc) -> bool { return exclude.count(rc.pubkey) == 0 and not r->router_profiling().IsBadForPath(rc.pubkey); }); if (not maybe.has_value()) return std::nullopt; return GetHopsForBuildWithEndpoint(maybe->router_id()); } std::optional> Endpoint::GetHopsForBuildWithEndpoint(RouterID endpoint) { return path::Builder::GetHopsAlignedToForBuild(endpoint, SnodeBlacklist()); } constexpr auto MaxOutboundContextPerRemote = 1; void Endpoint::PutNewOutboundContext(const service::IntroSet& introset, llarp_time_t) { const Address addr{introset.address_keys.Addr()}; auto& remoteSessions = _state->remote_sessions; if (remoteSessions.count(addr) < MaxOutboundContextPerRemote) { remoteSessions.emplace(addr, std::make_shared(introset, this)); LogInfo("Created New outbound context for ", addr.ToString()); } auto sessionRange = remoteSessions.equal_range(addr); for (auto itr = sessionRange.first; itr != sessionRange.second; ++itr) { // TODO: // itr->second->AddReadyHook( // [addr, this](auto session) { InformPathToService(addr, session); }, left); } } bool Endpoint::HasExit() const { for (const auto& [name, info] : _startup_ons_mappings) { if (info.first.has_value()) return true; } return not _exit_map.Empty(); } auto Endpoint::GetUniqueEndpointsForLookup() const { path::UniqueEndpointSet_t paths; ForEachPath([&paths](auto path) { if (path and path->IsReady()) paths.insert(path); }); return paths; } bool Endpoint::ReadyForNetwork() const { return is_ready() and ReadyToDoLookup(GetUniqueEndpointsForLookup().size()); } bool Endpoint::ReadyToDoLookup(size_t num_paths) const { // Currently just checks the number of paths, but could do more checks in the future. return num_paths >= MIN_ONS_LOOKUP_ENDPOINTS; } void Endpoint::lookup_name(std::string name, std::function func) { // TODO: so fuck all this? // if (not is_valid_name(name)) // { // handler(parse_address(name)); // return; // } // auto& cache = _state->nameCache; // const auto maybe = cache.Get(name); // if (maybe.has_value()) // { // handler(maybe); // return; // } log::info(link_cat, "{} looking up ONS name {}", Name(), name); auto paths = GetUniqueEndpointsForLookup(); // // not enough paths // if (not ReadyToDoLookup(paths.size())) // { // LogWarn( // Name(), // " not enough paths for lns lookup, have ", // paths.size(), // " need ", // MIN_ONS_LOOKUP_ENDPOINTS); // handler(std::nullopt); // return; // } // pick up to max_unique_lns_endpoints random paths to do lookups from std::vector chosenpaths; chosenpaths.insert(chosenpaths.begin(), paths.begin(), paths.end()); std::shuffle(chosenpaths.begin(), chosenpaths.end(), llarp::csrng); chosenpaths.resize(std::min(paths.size(), MAX_ONS_LOOKUP_ENDPOINTS)); for (const auto& path : chosenpaths) { log::info(link_cat, "{} lookup {} from {}", Name(), name, path->Endpoint()); path->find_name(name, func); } } void Endpoint::EnsureRouterIsKnown(const RouterID& rid) { if (rid.IsZero()) return; if (!router()->node_db()->has_router(rid)) { lookup_router(rid); } } bool Endpoint::lookup_router(RouterID rid, std::function func) { const auto& routers = _state->pending_routers; if (routers.find(rid) == routers.end()) { auto path = GetEstablishedPathClosestTo(rid); path->find_router("find_router", func); return true; } return false; } void Endpoint::HandlePathBuilt(path::Path_ptr p) { // p->SetDataHandler(util::memFn(&Endpoint::HandleHiddenServiceFrame, this)); // p->SetDropHandler(util::memFn(&Endpoint::HandleDataDrop, this)); // p->SetDeadChecker(util::memFn(&Endpoint::CheckPathIsDead, this)); path::Builder::HandlePathBuilt(p); } bool Endpoint::HandleDataDrop(path::Path_ptr p, const PathID_t& dst, uint64_t seq) { LogWarn(Name(), " message ", seq, " dropped by endpoint ", p->Endpoint(), " via ", dst); return true; } std::unordered_map Endpoint::NotifyParams() const { return {{"LOKINET_ADDR", _identity.pub.Addr().ToString()}}; } void Endpoint::FlushRecvData() { while (auto maybe = _recv_event_queue.tryPopFront()) { auto& ev = *maybe; ProtocolMessage::ProcessAsync(ev.fromPath, ev.pathid, ev.msg); } } void Endpoint::QueueRecvData(RecvDataEvent ev) { _recv_event_queue.tryPushBack(std::move(ev)); router()->TriggerPump(); } bool Endpoint::HandleDataMessage( path::Path_ptr p, const PathID_t from, std::shared_ptr msg) { PutSenderFor(msg->tag, msg->sender, true); Introduction intro = msg->introReply; if (HasInboundConvo(msg->sender.Addr())) { intro.path_id = from; intro.router = p->Endpoint(); } PutReplyIntroFor(msg->tag, intro); ConvoTagRX(msg->tag); return ProcessDataMessage(msg); } bool Endpoint::HasPathToSNode(const RouterID ident) const { auto range = _state->snode_sessions.equal_range(ident); auto itr = range.first; while (itr != range.second) { if (itr->second->IsReady()) { return true; } ++itr; } return false; } EndpointBase::AddressVariant_t Endpoint::LocalAddress() const { return _identity.pub.Addr(); } std::optional Endpoint::GetStatFor(AddressVariant_t) const { // TODO: implement me return std::nullopt; } std::unordered_set Endpoint::AllRemoteEndpoints() const { std::unordered_set remote; for (const auto& item : Sessions()) { remote.insert(item.second.remote.Addr()); } for (const auto& item : _state->snode_sessions) { remote.insert(item.first); } return remote; } bool Endpoint::ProcessDataMessage(std::shared_ptr msg) { if ((msg->proto == ProtocolType::Exit && (_state->is_exit_enabled || _exit_map.ContainsValue(msg->sender.Addr()))) || msg->proto == ProtocolType::TrafficV4 || msg->proto == ProtocolType::TrafficV6 || (msg->proto == ProtocolType::QUIC and _tunnel_manager)) { _inbound_queue.tryPushBack(std::move(msg)); router()->TriggerPump(); return true; } if (msg->proto == ProtocolType::Control) { // TODO: implement me (?) // right now it's just random noise return true; } return false; } void Endpoint::AsyncProcessAuthMessage( std::shared_ptr msg, std::function hook) { if (_auth_policy) { if (not _auth_policy->AsyncAuthPending(msg->tag)) { // do 1 authentication attempt and drop everything else _auth_policy->AuthenticateAsync(std::move(msg), std::move(hook)); } } else { router()->loop()->call([h = std::move(hook)] { h("OK", true); }); } } void Endpoint::SendAuthResult( path::Path_ptr path, PathID_t /* replyPath */, ConvoTag tag, std::string result, bool success) { // not applicable because we are not an exit or don't have an endpoint auth policy if ((not _state->is_exit_enabled) or _auth_policy == nullptr) return; ProtocolFrameMessage f{}; f.flag = int(not success); f.convo_tag = tag; f.path_id = path->intro.path_id; f.nonce.Randomize(); if (success) { ProtocolMessage msg; msg.put_buffer(result); if (_auth_policy) msg.proto = ProtocolType::Auth; else msg.proto = ProtocolType::Control; if (not GetReplyIntroFor(tag, msg.introReply)) { LogError("Failed to send auth reply: no reply intro"); return; } msg.sender = _identity.pub; SharedSecret sessionKey{}; if (not GetCachedSessionKeyFor(tag, sessionKey)) { LogError("failed to send auth reply: no cached session key"); return; } if (not f.EncryptAndSign(msg, sessionKey, _identity)) { LogError("Failed to encrypt and sign auth reply"); return; } } else { if (not f.Sign(_identity)) { LogError("failed to sign auth reply result"); return; } } // TODO: // _send_queue.tryPushBack( // SendEvent{std::make_shared(f, replyPath), path}); } void Endpoint::RemoveConvoTag(const ConvoTag& t) { Sessions().erase(t); } void Endpoint::ResetConvoTag(ConvoTag tag, path::Path_ptr p, PathID_t /* from */) { // send reset convo tag message ProtocolFrameMessage f{}; f.flag = 1; f.convo_tag = tag; f.path_id = p->intro.path_id; f.Sign(_identity); { LogWarn("invalidating convotag T=", tag); RemoveConvoTag(tag); // TODO: // _send_queue.tryPushBack( // SendEvent{std::make_shared(f, from), p}); } } bool Endpoint::HandleHiddenServiceFrame(path::Path_ptr p, const ProtocolFrameMessage& frame) { if (frame.flag) { // handle discard ServiceInfo si; if (!GetSenderFor(frame.convo_tag, si)) return false; // verify source if (!frame.Verify(si)) return false; // remove convotag it doesn't exist LogWarn("remove convotag T=", frame.convo_tag, " R=", frame.flag, " from ", si.Addr()); RemoveConvoTag(frame.convo_tag); return true; } if (not frame.AsyncDecryptAndVerify(router()->loop(), p, _identity, this)) { ResetConvoTag(frame.convo_tag, p, frame.path_id); } return true; } void Endpoint::HandlePathDied(path::Path_ptr p) { router()->router_profiling().MarkPathTimeout(p.get()); ManualRebuild(1); path::Builder::HandlePathDied(p); regen_and_publish_introset(); } bool Endpoint::CheckPathIsDead(path::Path_ptr, llarp_time_t dlt) { return dlt > path::ALIVE_TIMEOUT; } void Endpoint::MarkAddressOutbound(service::Address addr) { _state->m_OutboundSessions.insert(addr); } bool Endpoint::WantsOutboundSession(const Address& addr) const { return _state->m_OutboundSessions.count(addr) > 0; } void Endpoint::InformPathToService(const Address remote, OutboundContext* ctx) { auto& serviceLookups = _state->pending_service_lookups; auto range = serviceLookups.equal_range(remote); auto itr = range.first; while (itr != range.second) { itr->second(remote, ctx); ++itr; } serviceLookups.erase(remote); } bool Endpoint::EnsurePathTo( std::variant addr, std::function)> hook, llarp_time_t timeout) { if (auto ptr = std::get_if
(&addr)) { if (*ptr == _identity.pub.Addr()) { ConvoTag tag{}; if (auto maybe = GetBestConvoTagFor(*ptr)) tag = *maybe; else tag.Randomize(); PutSenderFor(tag, _identity.pub, true); ConvoTagTX(tag); Sessions()[tag].forever = true; Loop()->call_soon([tag, hook]() { hook(tag); }); return true; } if (not WantsOutboundSession(*ptr)) { // we don't want to connect back to inbound sessions hook(std::nullopt); return true; } return EnsurePathToService( *ptr, [hook](auto, auto* ctx) -> bool { if (ctx) { hook(ctx->get_current_tag()); return true; } hook(std::nullopt); return false; }, timeout); } if (auto ptr = std::get_if(&addr)) { return EnsurePathToSNode(*ptr, [hook](auto, auto session, auto tag) { if (session) { hook(tag); } else { hook(std::nullopt); } }); } return false; } bool Endpoint::EnsurePathToSNode( const RouterID snode, std::function hook) { auto& nodeSessions = _state->snode_sessions; using namespace std::placeholders; if (nodeSessions.count(snode) == 0) { const auto src = xhtonl(net::TruncateV6(GetIfAddr())); const auto dst = xhtonl(net::TruncateV6(ObtainIPForAddr(snode))); auto session = std::make_shared( snode, [=](const llarp_buffer_t& buf) -> bool { net::IPPacket pkt; if (not pkt.Load(buf)) return false; pkt.UpdateIPv4Address(src, dst); /// TODO: V6 auto itr = _state->snode_sessions.find(snode); if (itr == _state->snode_sessions.end()) return false; if (const auto maybe = itr->second->CurrentPath()) return HandleInboundPacket( ConvoTag{maybe->as_array()}, pkt.ConstBuffer(), ProtocolType::TrafficV4, 0); return false; }, router(), 1, numHops, false, this); _state->snode_sessions[snode] = session; } EnsureRouterIsKnown(snode); auto range = nodeSessions.equal_range(snode); auto itr = range.first; while (itr != range.second) { if (itr->second->IsReady()) hook(snode, itr->second, ConvoTag{itr->second->CurrentPath()->as_array()}); else { itr->second->AddReadyHook([hook, snode](auto session) { if (session) { hook(snode, session, ConvoTag{session->CurrentPath()->as_array()}); } else { hook(snode, nullptr, ConvoTag{}); } }); if (not itr->second->BuildCooldownHit(Now())) itr->second->BuildOne(); } ++itr; } return true; } bool Endpoint::EnsurePathToService( const Address remote, std::function hook, [[maybe_unused]] llarp_time_t timeout) { if (not WantsOutboundSession(remote)) { // we don't want to ensure paths to addresses that are inbound // inform fail right away in that case hook(remote, nullptr); return false; } /// how many routers to use for lookups static constexpr size_t NumParallelLookups = 2; // add response hook to list for address. _state->pending_service_lookups.emplace(remote, hook); auto& sessions = _state->remote_sessions; { auto range = sessions.equal_range(remote); auto itr = range.first; while (itr != range.second) { if (itr->second->ReadyToSend()) { InformPathToService(remote, itr->second.get()); return true; } ++itr; } } /// check replay filter if (not _introset_lookup_filter.Insert(remote)) return true; const auto paths = GetManyPathsWithUniqueEndpoints(this, NumParallelLookups); const dht::Key_t location = remote.ToKey(); // flag to only add callback to list of callbacks for // address once. bool hookAdded = false; for (const auto& path : paths) { path->find_intro(location, false, 0, [this, hook](oxen::quic::message m) mutable { if (m) { std::string introset; try { oxenc::bt_dict_consumer btdc{m.body()}; introset = btdc.require("INTROSET"); } catch (...) { log::warning(link_cat, "Failed to parse find name response!"); throw; } service::EncryptedIntroSet enc{introset}; router()->contacts()->services()->PutNode(std::move(enc)); // TODO: finish this } }); } return hookAdded; } void Endpoint::SRVRecordsChanged() { auto& introset = intro_set(); introset.SRVs.clear(); for (const auto& srv : SRVRecords()) introset.SRVs.emplace_back(srv.toTuple()); regen_and_publish_introset(); } bool Endpoint::send_to(ConvoTag tag, std::string payload) { if (tag.IsZero()) { log::warning(link_cat, "SendToOrQueue failed: convo tag is zero"); return false; } log::debug(link_cat, "{} sending {} bytes (Tag: {})", Name(), payload.size(), tag); if (auto maybe = GetEndpointWithConvoTag(tag)) { if (auto rid = std::get_if(&*maybe)) { return router()->send_data_message(*rid, payload); } if (auto saddr = std::get_if
(&*maybe)) { return router()->send_data_message(saddr->ToRouter(), payload); } } log::debug(link_cat, "SendToOrQueue failed: no endpoint for convo tag {}", tag); return false; } void Endpoint::Pump(llarp_time_t) { FlushRecvData(); // send downstream packets to user for snode for (const auto& [router, session] : _state->snode_sessions) session->FlushDownstream(); // handle inbound traffic sorted util::ascending_priority_queue queue; while (not _inbound_queue.empty()) { // succ it out queue.emplace(std::move(*_inbound_queue.popFront())); } while (not queue.empty()) { const auto& msg = queue.top(); log::debug( logcat, "{} handling inbound packet (size {}B) on tag {}", Name(), msg.tag, msg.payload.size()); // if (HandleInboundPacket(msg.tag, msg.payload, msg.proto, msg.seqno)) // { // ConvoTagRX(msg.tag); // } // else // { // LogWarn("Failed to handle inbound message"); // } queue.pop(); } auto r = router(); // TODO: locking on this container // for (const auto& [addr, outctx] : _state->remote_sessions) // { // outctx->FlushUpstream(); // outctx->Pump(now); // } // // TODO: locking on this container // for (const auto& [r, session] : _state->snode_sessions) // session->FlushUpstream(); // // send queue flush // while (not _send_queue.empty()) // { // SendEvent item = _send_queue.popFront(); // item.first->sequence_number = item.second->NextSeqNo(); // if (item.second->SendRoutingMessage(*item.first, r)) // ConvoTagTX(item.first->protocol_frame_msg.convo_tag); // } UpstreamFlush(r); } std::optional Endpoint::GetBestConvoTagFor(std::variant remote) const { // get convotag with lowest estimated RTT if (auto ptr = std::get_if
(&remote)) { llarp_time_t rtt = 30s; std::optional ret = std::nullopt; for (const auto& [tag, session] : Sessions()) { if (tag.IsZero()) continue; if (session.remote.Addr() == *ptr) { if (*ptr == _identity.pub.Addr()) { return tag; } if (session.inbound) { auto path = GetPathByRouter(session.replyIntro.router); // if we have no path to the remote router that's fine still use it just in case this // is the ONLY one we have if (path == nullptr) { ret = tag; continue; } if (path and path->IsReady()) { const auto rttEstimate = (session.replyIntro.latency + path->intro.latency) * 2; if (rttEstimate < rtt) { ret = tag; rtt = rttEstimate; } } } else { auto range = _state->remote_sessions.equal_range(*ptr); auto itr = range.first; while (itr != range.second) { // TODO: // if (itr->second->ReadyToSend() and itr->second->estimatedRTT > 0s) // { // if (itr->second->estimatedRTT < rtt) // { // ret = tag; // rtt = itr->second->estimatedRTT; // } // } itr++; } } } } return ret; } if (auto* ptr = std::get_if(&remote)) { auto itr = _state->snode_sessions.find(*ptr); if (itr == _state->snode_sessions.end()) return std::nullopt; if (auto maybe = itr->second->CurrentPath()) return ConvoTag{maybe->as_array()}; } return std::nullopt; } bool Endpoint::HasConvoTag(const ConvoTag& t) const { return Sessions().find(t) != Sessions().end(); } std::optional Endpoint::GetSeqNoForConvo(const ConvoTag& tag) { auto itr = Sessions().find(tag); if (itr == Sessions().end()) return std::nullopt; return itr->second.seqno++; } bool Endpoint::ShouldBuildMore(llarp_time_t now) const { if (BuildCooldownHit(now)) return false; const auto requiredPaths = std::max(numDesiredPaths, path::MIN_INTRO_PATHS); if (NumInStatus(path::ePathBuilding) >= requiredPaths) return false; return NumPathsExistingAt(now + (path::DEFAULT_LIFETIME - path::INTRO_PATH_SPREAD)) < requiredPaths; } Router* Endpoint::router() { return _state->router; } const EventLoop_ptr& Endpoint::Loop() { return router()->loop(); } void Endpoint::BlacklistSNode(const RouterID snode) { _state->snode_blacklist.insert(snode); } const std::set& Endpoint::SnodeBlacklist() const { return _state->snode_blacklist; } const IntroSet& Endpoint::intro_set() const { return _state->local_introset; } IntroSet& Endpoint::intro_set() { return _state->local_introset; } const std::unordered_map& Endpoint::Sessions() const { return _state->m_Sessions; } std::unordered_map& Endpoint::Sessions() { return _state->m_Sessions; } void Endpoint::SetAuthInfoForEndpoint(Address addr, AuthInfo info) { if (info.token.empty()) { _remote_auth_infos.erase(addr); return; } _remote_auth_infos[addr] = std::move(info); } void Endpoint::MapExitRange(IPRange range, Address exit) { if (not exit.IsZero()) LogInfo(Name(), " map ", range, " to exit at ", exit); _exit_map.Insert(range, exit); } bool Endpoint::HasFlowToService(Address addr) const { return HasOutboundConvo(addr) or HasInboundConvo(addr); } void Endpoint::UnmapExitRange(IPRange range) { // unmap all ranges that fit in the range we gave _exit_map.RemoveIf([&](const auto& item) -> bool { if (not range.Contains(item.first)) return false; LogInfo(Name(), " unmap ", item.first, " exit range mapping"); return true; }); if (_exit_map.Empty()) router()->route_poker()->put_down(); } void Endpoint::UnmapRangeByExit(IPRange range, std::string exit) { // unmap all ranges that match the given exit when hot swapping _exit_map.RemoveIf([&](const auto& item) -> bool { if ((range.Contains(item.first)) and (item.second.ToString() == exit)) { log::info(logcat, "{} unmap {} range mapping to exit node {}", Name(), item.first, exit); return true; } return false; }); if (_exit_map.Empty()) router()->route_poker()->put_down(); } std::optional Endpoint::MaybeGetAuthInfoForEndpoint(Address remote) { const auto itr = _remote_auth_infos.find(remote); if (itr == _remote_auth_infos.end()) return std::nullopt; return itr->second; } link::TunnelManager* Endpoint::GetQUICTunnel() { return _tunnel_manager.get(); } } // namespace llarp::service