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