mirror of https://github.com/oxen-io/lokinet
LNS (#1342)
* initial relay side lns * fix typo * add reserved names and refactor test for dns * lns name decryption * all wired up (allegedly) * refact to use service::EncryptedName for LNS responses to include nonce with ciphertext * fully rwemove tag_lookup_job * replace lns cache with DecayingHashTable * check for lns name validity against the following rules: * not localhost.loki, loki.loki, or snode.loki * if it contains no dash then max 32 characters long, not including the .loki tld (and also assuming a leading subdomain has been stripped) * These are from general DNS requirements, and also enforced in registrations: * Must be all [A-Za-z0-9-]. (A-Z will be lower-cased by the RPC call). * cannot start or end with a - * max 63 characters long if it does contain a dash * cannot contain -- in the third and fourth characters unless it starts with xn-- * handle timeout in name lookup job by calling the right handler with std::nulloptpull/1355/head
parent
f5e5066bd5
commit
21930cf667
@ -0,0 +1,66 @@
|
||||
#include <dht/messages/findname.hpp>
|
||||
#include <lokimq/bt_serialize.h>
|
||||
#include <dht/context.hpp>
|
||||
#include <dht/messages/gotname.hpp>
|
||||
#include <router/abstractrouter.hpp>
|
||||
#include <rpc/lokid_rpc_client.hpp>
|
||||
#include <path/path_context.hpp>
|
||||
#include <routing/dht_message.hpp>
|
||||
|
||||
namespace llarp::dht
|
||||
{
|
||||
FindNameMessage::FindNameMessage(const Key_t& from, Key_t namehash, uint64_t txid)
|
||||
: IMessage(from), NameHash(std::move(namehash)), TxID(txid)
|
||||
{
|
||||
}
|
||||
|
||||
bool
|
||||
FindNameMessage::BEncode(llarp_buffer_t* buf) const
|
||||
{
|
||||
const auto data = lokimq::bt_serialize(
|
||||
lokimq::bt_dict{{"A", "N"sv},
|
||||
{"H", std::string_view{(char*)NameHash.data(), NameHash.size()}},
|
||||
{"T", TxID}});
|
||||
return buf->write(data.begin(), data.end());
|
||||
}
|
||||
|
||||
bool
|
||||
FindNameMessage::DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* val)
|
||||
{
|
||||
if (key == "H")
|
||||
{
|
||||
return NameHash.BDecode(val);
|
||||
}
|
||||
if (key == "T")
|
||||
{
|
||||
return bencode_read_integer(val, &TxID);
|
||||
}
|
||||
return bencode_discard(val);
|
||||
}
|
||||
|
||||
bool
|
||||
FindNameMessage::HandleMessage(struct llarp_dht_context* dht, std::vector<Ptr_t>& replies) const
|
||||
{
|
||||
(void)replies;
|
||||
auto r = dht->impl->GetRouter();
|
||||
if (pathID.IsZero() or not r->IsServiceNode())
|
||||
return false;
|
||||
r->RpcClient()->LookupLNSNameHash(NameHash, [r, pathID = pathID, TxID = TxID](auto maybe) {
|
||||
auto path = r->pathContext().GetPathForTransfer(pathID);
|
||||
if (path == nullptr)
|
||||
return;
|
||||
routing::DHTMessage msg;
|
||||
if (maybe.has_value())
|
||||
{
|
||||
msg.M.emplace_back(new GotNameMessage(dht::Key_t{}, TxID, *maybe));
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.M.emplace_back(new GotNameMessage(dht::Key_t{}, TxID, service::EncryptedName{}));
|
||||
}
|
||||
path->SendRoutingMessage(msg, r);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace llarp::dht
|
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <dht/message.hpp>
|
||||
|
||||
namespace llarp::dht
|
||||
{
|
||||
struct FindNameMessage : public IMessage
|
||||
{
|
||||
explicit FindNameMessage(const Key_t& from, Key_t namehash, uint64_t txid);
|
||||
|
||||
bool
|
||||
BEncode(llarp_buffer_t* buf) const override;
|
||||
|
||||
bool
|
||||
DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* val) override;
|
||||
|
||||
bool
|
||||
HandleMessage(struct llarp_dht_context* dht, std::vector<Ptr_t>& replies) const override;
|
||||
|
||||
Key_t NameHash;
|
||||
uint64_t TxID;
|
||||
};
|
||||
|
||||
} // namespace llarp::dht
|
@ -0,0 +1,55 @@
|
||||
#include <dht/messages/gotname.hpp>
|
||||
#include <lokimq/bt_serialize.h>
|
||||
|
||||
namespace llarp::dht
|
||||
{
|
||||
constexpr size_t NameSizeLimit = 128;
|
||||
|
||||
GotNameMessage::GotNameMessage(const Key_t& from, uint64_t txid, service::EncryptedName data)
|
||||
: IMessage(from), result(std::move(data)), TxID(txid)
|
||||
{
|
||||
if (result.ciphertext.size() > NameSizeLimit)
|
||||
throw std::invalid_argument("name data too big");
|
||||
}
|
||||
|
||||
bool
|
||||
GotNameMessage::BEncode(llarp_buffer_t* buf) const
|
||||
{
|
||||
const std::string nonce((const char*)result.nonce.data(), result.nonce.size());
|
||||
const auto data = lokimq::bt_serialize(
|
||||
lokimq::bt_dict{{"A", "M"sv}, {"D", result.ciphertext}, {"N", nonce}, {"T", TxID}});
|
||||
return buf->write(data.begin(), data.end());
|
||||
}
|
||||
|
||||
bool
|
||||
GotNameMessage::DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* val)
|
||||
{
|
||||
if (key == "D")
|
||||
{
|
||||
llarp_buffer_t str{};
|
||||
if (not bencode_read_string(val, &str))
|
||||
return false;
|
||||
if (str.sz > NameSizeLimit)
|
||||
return false;
|
||||
result.ciphertext.resize(str.sz);
|
||||
std::copy_n(str.cur, str.sz, result.ciphertext.data());
|
||||
return true;
|
||||
}
|
||||
if (key == "N")
|
||||
{
|
||||
return result.nonce.BDecode(val);
|
||||
}
|
||||
if (key == "T")
|
||||
{
|
||||
return bencode_read_integer(val, &TxID);
|
||||
}
|
||||
return bencode_discard(val);
|
||||
}
|
||||
|
||||
bool
|
||||
GotNameMessage::HandleMessage(struct llarp_dht_context*, std::vector<Ptr_t>&) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace llarp::dht
|
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <dht/message.hpp>
|
||||
#include <service/name.hpp>
|
||||
|
||||
namespace llarp::dht
|
||||
{
|
||||
struct GotNameMessage : public IMessage
|
||||
{
|
||||
explicit GotNameMessage(const Key_t& from, uint64_t txid, service::EncryptedName data);
|
||||
|
||||
bool
|
||||
BEncode(llarp_buffer_t* buf) const override;
|
||||
|
||||
bool
|
||||
DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* val) override;
|
||||
|
||||
bool
|
||||
HandleMessage(struct llarp_dht_context* dht, std::vector<Ptr_t>& replies) const override;
|
||||
|
||||
service::EncryptedName result;
|
||||
uint64_t TxID;
|
||||
};
|
||||
|
||||
} // namespace llarp::dht
|
@ -0,0 +1,66 @@
|
||||
#include <service/name.hpp>
|
||||
#include <crypto/crypto.hpp>
|
||||
#include <util/str.hpp>
|
||||
|
||||
namespace llarp::service
|
||||
{
|
||||
std::optional<Address>
|
||||
EncryptedName::Decrypt(std::string_view name) const
|
||||
{
|
||||
if (ciphertext.empty())
|
||||
return std::nullopt;
|
||||
const auto crypto = CryptoManager::instance();
|
||||
const auto maybe = crypto->maybe_decrypt_name(ciphertext, nonce, name);
|
||||
if (maybe.has_value())
|
||||
return Address{*maybe};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool
|
||||
NameIsValid(std::string_view lnsName)
|
||||
{
|
||||
// strip off .loki suffix
|
||||
lnsName = lnsName.substr(0, lnsName.find_last_of('.'));
|
||||
|
||||
// ensure chars are sane
|
||||
for (const auto ch : lnsName)
|
||||
{
|
||||
if (ch == '-')
|
||||
continue;
|
||||
if (ch == '.')
|
||||
continue;
|
||||
if (ch >= 'a' and ch <= 'z')
|
||||
continue;
|
||||
if (ch >= '0' and ch <= '9')
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
// split into domain parts
|
||||
const auto parts = split(lnsName, ".");
|
||||
// get root domain
|
||||
const auto primaryName = parts[parts.size() - 1];
|
||||
constexpr size_t MaxNameLen = 32;
|
||||
constexpr size_t MaxPunycodeNameLen = 63;
|
||||
// check against lns name blacklist
|
||||
if (primaryName == "localhost")
|
||||
return false;
|
||||
if (primaryName == "loki")
|
||||
return false;
|
||||
if (primaryName == "snode")
|
||||
return false;
|
||||
// check for dashes
|
||||
if (primaryName.find("-") == std::string_view::npos)
|
||||
return primaryName.size() <= MaxNameLen;
|
||||
// check for dashes and end or beginning
|
||||
if (*primaryName.begin() == '-' or *(primaryName.end() - 1) == '-')
|
||||
return false;
|
||||
// check for punycode name length
|
||||
if (primaryName.size() > MaxPunycodeNameLen)
|
||||
return false;
|
||||
// check for xn--
|
||||
return (primaryName[2] == '-' and primaryName[3] == '-')
|
||||
? (primaryName[0] == 'x' and primaryName[1] == 'n')
|
||||
: true;
|
||||
}
|
||||
|
||||
} // namespace llarp::service
|
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
#include <crypto/types.hpp>
|
||||
#include <service/address.hpp>
|
||||
|
||||
namespace llarp::service
|
||||
{
|
||||
struct EncryptedName
|
||||
{
|
||||
SymmNonce nonce;
|
||||
std::string ciphertext;
|
||||
|
||||
std::optional<Address>
|
||||
Decrypt(std::string_view name) const;
|
||||
};
|
||||
|
||||
/// check if an lns name complies with the registration rules
|
||||
bool
|
||||
NameIsValid(std::string_view name);
|
||||
|
||||
} // namespace llarp::service
|
@ -1,50 +0,0 @@
|
||||
#include <service/tag_lookup_job.hpp>
|
||||
|
||||
#include <dht/messages/findintro.hpp>
|
||||
#include <routing/dht_message.hpp>
|
||||
#include <service/endpoint.hpp>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
namespace service
|
||||
{
|
||||
bool
|
||||
CachedTagResult::HandleResponse(const std::set<EncryptedIntroSet>&)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
CachedTagResult::Expire(llarp_time_t now)
|
||||
{
|
||||
auto itr = result.begin();
|
||||
while (itr != result.end())
|
||||
{
|
||||
if (itr->IsExpired(now))
|
||||
{
|
||||
itr = result.erase(itr);
|
||||
lastModified = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
++itr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<routing::IMessage>
|
||||
CachedTagResult::BuildRequestMessage(uint64_t txid)
|
||||
{
|
||||
auto msg = std::make_shared<routing::DHTMessage>();
|
||||
msg->M.emplace_back(std::make_unique<dht::FindIntroMessage>(tag, txid));
|
||||
lastRequest = m_parent->Now();
|
||||
return msg;
|
||||
}
|
||||
|
||||
TagLookupJob::TagLookupJob(Endpoint* parent, CachedTagResult* result)
|
||||
: IServiceLookup(parent, parent->GenTXID(), "taglookup"), m_result(result)
|
||||
{
|
||||
}
|
||||
} // namespace service
|
||||
|
||||
} // namespace llarp
|
@ -1,75 +0,0 @@
|
||||
#ifndef LLARP_SERVICE_TAG_LOOKUP_JOB_HPP
|
||||
#define LLARP_SERVICE_TAG_LOOKUP_JOB_HPP
|
||||
|
||||
#include <routing/message.hpp>
|
||||
#include <service/intro_set.hpp>
|
||||
#include <service/lookup.hpp>
|
||||
#include <service/tag.hpp>
|
||||
#include <util/types.hpp>
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace llarp
|
||||
{
|
||||
namespace service
|
||||
{
|
||||
struct Endpoint;
|
||||
|
||||
struct CachedTagResult
|
||||
{
|
||||
static constexpr auto TTL = 10s;
|
||||
llarp_time_t lastRequest = 0s;
|
||||
llarp_time_t lastModified = 0s;
|
||||
std::set<EncryptedIntroSet> result;
|
||||
Tag tag;
|
||||
Endpoint* m_parent;
|
||||
|
||||
CachedTagResult(const Tag& t, Endpoint* p) : tag(t), m_parent(p)
|
||||
{
|
||||
}
|
||||
|
||||
~CachedTagResult() = default;
|
||||
|
||||
void
|
||||
Expire(llarp_time_t now);
|
||||
|
||||
bool
|
||||
ShouldRefresh(llarp_time_t now) const
|
||||
{
|
||||
if (now <= lastRequest)
|
||||
return false;
|
||||
return (now - lastRequest) > TTL;
|
||||
}
|
||||
|
||||
std::shared_ptr<routing::IMessage>
|
||||
BuildRequestMessage(uint64_t txid);
|
||||
|
||||
bool
|
||||
HandleResponse(const std::set<EncryptedIntroSet>& results);
|
||||
};
|
||||
|
||||
struct TagLookupJob : public IServiceLookup
|
||||
{
|
||||
TagLookupJob(Endpoint* parent, CachedTagResult* result);
|
||||
|
||||
~TagLookupJob() override = default;
|
||||
|
||||
std::shared_ptr<routing::IMessage>
|
||||
BuildRequestMessage() override
|
||||
{
|
||||
return m_result->BuildRequestMessage(txid);
|
||||
}
|
||||
|
||||
bool
|
||||
HandleResponse(const std::set<EncryptedIntroSet>& results) override
|
||||
{
|
||||
return m_result->HandleResponse(results);
|
||||
}
|
||||
|
||||
CachedTagResult* m_result;
|
||||
};
|
||||
|
||||
} // namespace service
|
||||
} // namespace llarp
|
||||
|
||||
#endif
|
@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <util/time.hpp>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace llarp::util
|
||||
{
|
||||
template <typename Key_t, typename Value_t, typename Hash_t = typename Key_t::Hash>
|
||||
struct DecayingHashTable
|
||||
{
|
||||
DecayingHashTable(std::chrono::milliseconds cacheInterval = 1h) : m_CacheInterval(cacheInterval)
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Decay(llarp_time_t now)
|
||||
{
|
||||
EraseIf([&](const auto& item) { return item.second.second + m_CacheInterval <= now; });
|
||||
}
|
||||
|
||||
bool
|
||||
Has(const Key_t& k) const
|
||||
{
|
||||
return m_Values.find(k) != m_Values.end();
|
||||
}
|
||||
|
||||
/// return true if inserted
|
||||
/// return false if not inserted
|
||||
bool
|
||||
Put(Key_t key, Value_t value, llarp_time_t now = 0s)
|
||||
{
|
||||
if (now == 0s)
|
||||
now = llarp::time_now_ms();
|
||||
return m_Values.try_emplace(std::move(key), std::make_pair(std::move(value), now)).second;
|
||||
}
|
||||
|
||||
std::optional<Value_t>
|
||||
Get(Key_t k) const
|
||||
{
|
||||
const auto itr = m_Values.find(k);
|
||||
if (itr == m_Values.end())
|
||||
return std::nullopt;
|
||||
return itr->second.first;
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Predicate_t>
|
||||
void
|
||||
EraseIf(Predicate_t pred)
|
||||
{
|
||||
for (auto i = m_Values.begin(), last = m_Values.end(); i != last;)
|
||||
{
|
||||
if (pred(*i))
|
||||
{
|
||||
i = m_Values.erase(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
llarp_time_t m_CacheInterval;
|
||||
std::unordered_map<Key_t, std::pair<Value_t, llarp_time_t>, Hash_t> m_Values;
|
||||
};
|
||||
} // namespace llarp::util
|
@ -0,0 +1,43 @@
|
||||
#include "catch2/catch.hpp"
|
||||
#include <crypto/crypto_libsodium.hpp>
|
||||
#include <service/name.hpp>
|
||||
#include <lokimq/hex.h>
|
||||
|
||||
using namespace std::literals;
|
||||
|
||||
TEST_CASE("Test LNS name decrypt", "[lns]")
|
||||
{
|
||||
llarp::sodium::CryptoLibSodium crypto;
|
||||
constexpr auto recordhex = "0ba76cbfdb6dc8f950da57ae781912f31c8ad0c55dbf86b88cb0391f563261a9656571a817be4092969f8a78ee0fcee260424acb4a1f4bbdd27348b71de006b6152dd04ed11bf3c4"sv;
|
||||
const auto recordbin = lokimq::from_hex(recordhex);
|
||||
CHECK(not recordbin.empty());
|
||||
llarp::SymmNonce n{};
|
||||
std::vector<byte_t> ciphertext{};
|
||||
const auto len = recordbin.size() - n.size();
|
||||
std::copy_n(recordbin.cbegin() + len, n.size(), n.data());
|
||||
std::copy_n(recordbin.cbegin(), len, std::back_inserter(ciphertext));
|
||||
const auto maybe = crypto.maybe_decrypt_name(std::string_view{reinterpret_cast<const char *>(ciphertext.data()), ciphertext.size()}, n, "jason.loki");
|
||||
CHECK(maybe.has_value());
|
||||
const llarp::service::Address addr{*maybe};
|
||||
CHECK(addr.ToString() == "azfoj73snr9f3neh5c6sf7rtbaeabyxhr1m4un5aydsmsrxo964o.loki");
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE("Test LNS validity", "[lns]")
|
||||
{
|
||||
CHECK(not llarp::service::NameIsValid("loki.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("snode.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("localhost.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("gayballs22.loki.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("-loki.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("super-mario-gayballs-.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("bn--lolexdeeeeee.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("2222222222a-.loki"));
|
||||
CHECK(not llarp::service::NameIsValid("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.loki"));
|
||||
|
||||
CHECK(llarp::service::NameIsValid("xn--animewasindeedamistake.loki"));
|
||||
CHECK(llarp::service::NameIsValid("memerionos.loki"));
|
||||
CHECK(llarp::service::NameIsValid("whyis.xn--animehorrible.loki"));
|
||||
CHECK(llarp::service::NameIsValid("the.goog.loki"));
|
||||
CHECK(llarp::service::NameIsValid("420.loki"));
|
||||
}
|
Loading…
Reference in New Issue