mirror of
https://github.com/monero-project/monero.git
synced 2025-09-17 13:54:38 -04:00
Merge pull request #5793
bdfc63a
Add ref-counted buffer byte_slice. Currently used for sending TCP data. (vtnerd)3b24b1d
Added support for 'noise' over I1P/Tor to mask Tx transmission. (vtnerd)
This commit is contained in:
commit
98af2e954b
35 changed files with 3576 additions and 278 deletions
|
@ -31,8 +31,10 @@
|
|||
#pragma once
|
||||
#include <unordered_set>
|
||||
#include <atomic>
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include "net/net_utils_base.h"
|
||||
#include "copyable_atomic.h"
|
||||
#include "crypto/hash.h"
|
||||
|
||||
namespace cryptonote
|
||||
{
|
||||
|
|
|
@ -100,6 +100,16 @@
|
|||
#define CRYPTONOTE_MEMPOOL_TX_LIVETIME (86400*3) //seconds, three days
|
||||
#define CRYPTONOTE_MEMPOOL_TX_FROM_ALT_BLOCK_LIVETIME 604800 //seconds, one week
|
||||
|
||||
// see src/cryptonote_protocol/levin_notify.cpp
|
||||
#define CRYPTONOTE_NOISE_MIN_EPOCH 5 // minutes
|
||||
#define CRYPTONOTE_NOISE_EPOCH_RANGE 30 // seconds
|
||||
#define CRYPTONOTE_NOISE_MIN_DELAY 10 // seconds
|
||||
#define CRYPTONOTE_NOISE_DELAY_RANGE 5 // seconds
|
||||
#define CRYPTONOTE_NOISE_BYTES 3*1024 // 3 KiB
|
||||
#define CRYPTONOTE_NOISE_CHANNELS 2 // Max outgoing connections per zone used for noise/covert sending
|
||||
|
||||
#define CRYPTONOTE_MAX_FRAGMENTS 20 // ~20 * NOISE_BYTES max payload size for covert/noise send
|
||||
|
||||
#define COMMAND_RPC_GET_BLOCKS_FAST_MAX_COUNT 1000
|
||||
|
||||
#define P2P_LOCAL_WHITE_PEERLIST_LIMIT 1000
|
||||
|
|
|
@ -2227,69 +2227,11 @@ skip:
|
|||
template<class t_core>
|
||||
bool t_cryptonote_protocol_handler<t_core>::relay_transactions(NOTIFY_NEW_TRANSACTIONS::request& arg, cryptonote_connection_context& exclude_context)
|
||||
{
|
||||
const bool hide_tx_broadcast =
|
||||
1 < m_p2p->get_zone_count() && exclude_context.m_remote_address.get_zone() == epee::net_utils::zone::invalid;
|
||||
|
||||
if (hide_tx_broadcast)
|
||||
MDEBUG("Attempting to conceal origin of tx via anonymity network connection(s)");
|
||||
for(auto& tx_blob : arg.txs)
|
||||
m_core.on_transaction_relayed(tx_blob);
|
||||
|
||||
// no check for success, so tell core they're relayed unconditionally
|
||||
const bool pad_transactions = m_core.pad_transactions() || hide_tx_broadcast;
|
||||
size_t bytes = pad_transactions ? 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(arg.txs.size()).size() : 0;
|
||||
for(auto tx_blob_it = arg.txs.begin(); tx_blob_it!=arg.txs.end(); ++tx_blob_it)
|
||||
{
|
||||
m_core.on_transaction_relayed(*tx_blob_it);
|
||||
if (pad_transactions)
|
||||
bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size();
|
||||
}
|
||||
|
||||
if (pad_transactions)
|
||||
{
|
||||
// stuff some dummy bytes in to stay safe from traffic volume analysis
|
||||
static constexpr size_t granularity = 1024;
|
||||
size_t padding = granularity - bytes % granularity;
|
||||
const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size();
|
||||
if (overhead > padding)
|
||||
padding = 0;
|
||||
else
|
||||
padding -= overhead;
|
||||
arg._ = std::string(padding, ' ');
|
||||
|
||||
std::string arg_buff;
|
||||
epee::serialization::store_t_to_binary(arg, arg_buff);
|
||||
|
||||
// we probably lowballed the payload size a bit, so added a but too much. Fix this now.
|
||||
size_t remove = arg_buff.size() % granularity;
|
||||
if (remove > arg._.size())
|
||||
arg._.clear();
|
||||
else
|
||||
arg._.resize(arg._.size() - remove);
|
||||
// if the size of _ moved enough, we might lose byte in size encoding, we don't care
|
||||
}
|
||||
|
||||
std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections;
|
||||
m_p2p->for_each_connection([hide_tx_broadcast, &exclude_context, &connections](connection_context& context, nodetool::peerid_type peer_id, uint32_t support_flags)
|
||||
{
|
||||
const epee::net_utils::zone current_zone = context.m_remote_address.get_zone();
|
||||
const bool broadcast_to_peer =
|
||||
peer_id &&
|
||||
(hide_tx_broadcast != bool(current_zone == epee::net_utils::zone::public_)) &&
|
||||
exclude_context.m_connection_id != context.m_connection_id;
|
||||
|
||||
if (broadcast_to_peer)
|
||||
connections.push_back({current_zone, context.m_connection_id});
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (connections.empty())
|
||||
MERROR("Transaction not relayed - no" << (hide_tx_broadcast ? " privacy": "") << " peers available");
|
||||
else
|
||||
{
|
||||
std::string fullBlob;
|
||||
epee::serialization::store_t_to_binary(arg, fullBlob);
|
||||
m_p2p->relay_notify_to_list(NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<uint8_t>(fullBlob), std::move(connections));
|
||||
}
|
||||
m_p2p->send_txs(std::move(arg.txs), exclude_context.m_remote_address.get_zone(), exclude_context.m_connection_id, m_core.pad_transactions());
|
||||
return true;
|
||||
}
|
||||
//------------------------------------------------------------------------------------------------------------------------
|
||||
|
|
574
src/cryptonote_protocol/levin_notify.cpp
Normal file
574
src/cryptonote_protocol/levin_notify.cpp
Normal file
|
@ -0,0 +1,574 @@
|
|||
// Copyright (c) 2019, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#include "levin_notify.h"
|
||||
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/system/system_error.hpp>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "common/expect.h"
|
||||
#include "common/varint.h"
|
||||
#include "cryptonote_config.h"
|
||||
#include "crypto/random.h"
|
||||
#include "cryptonote_basic/connection_context.h"
|
||||
#include "cryptonote_protocol/cryptonote_protocol_defs.h"
|
||||
#include "net/dandelionpp.h"
|
||||
#include "p2p/net_node.h"
|
||||
|
||||
namespace cryptonote
|
||||
{
|
||||
namespace levin
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t connection_id_reserve_size = 100;
|
||||
|
||||
constexpr const std::chrono::minutes noise_min_epoch{CRYPTONOTE_NOISE_MIN_EPOCH};
|
||||
constexpr const std::chrono::seconds noise_epoch_range{CRYPTONOTE_NOISE_EPOCH_RANGE};
|
||||
|
||||
constexpr const std::chrono::seconds noise_min_delay{CRYPTONOTE_NOISE_MIN_DELAY};
|
||||
constexpr const std::chrono::seconds noise_delay_range{CRYPTONOTE_NOISE_DELAY_RANGE};
|
||||
|
||||
/*! Select a randomized duration from 0 to `range`. The precision will be to
|
||||
the systems `steady_clock`. As an example, supplying 3 seconds to this
|
||||
function will select a duration from [0, 3] seconds, and the increments
|
||||
for the selection will be determined by the `steady_clock` precision
|
||||
(typically nanoseconds).
|
||||
|
||||
\return A randomized duration from 0 to `range`. */
|
||||
std::chrono::steady_clock::duration random_duration(std::chrono::steady_clock::duration range)
|
||||
{
|
||||
using rep = std::chrono::steady_clock::rep;
|
||||
return std::chrono::steady_clock::duration{crypto::rand_range(rep(0), range.count())};
|
||||
}
|
||||
|
||||
//! \return All outgoing connections supporting fragments in `connections`.
|
||||
std::vector<boost::uuids::uuid> get_out_connections(connections& p2p)
|
||||
{
|
||||
std::vector<boost::uuids::uuid> outs;
|
||||
outs.reserve(connection_id_reserve_size);
|
||||
|
||||
/* The foreach call is serialized with a lock, but should be quick due to
|
||||
the reserve call so a strand is not used. Investigate if there is lots
|
||||
of waiting in here. */
|
||||
|
||||
p2p.foreach_connection([&outs] (detail::p2p_context& context) {
|
||||
if (!context.m_is_income)
|
||||
outs.emplace_back(context.m_connection_id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return outs;
|
||||
}
|
||||
|
||||
std::string make_tx_payload(std::vector<blobdata>&& txs, const bool pad)
|
||||
{
|
||||
NOTIFY_NEW_TRANSACTIONS::request request{};
|
||||
request.txs = std::move(txs);
|
||||
|
||||
if (pad)
|
||||
{
|
||||
size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size();
|
||||
for(auto tx_blob_it = request.txs.begin(); tx_blob_it!=request.txs.end(); ++tx_blob_it)
|
||||
bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size();
|
||||
|
||||
// stuff some dummy bytes in to stay safe from traffic volume analysis
|
||||
static constexpr const size_t granularity = 1024;
|
||||
size_t padding = granularity - bytes % granularity;
|
||||
const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size();
|
||||
if (overhead > padding)
|
||||
padding = 0;
|
||||
else
|
||||
padding -= overhead;
|
||||
request._ = std::string(padding, ' ');
|
||||
|
||||
std::string arg_buff;
|
||||
epee::serialization::store_t_to_binary(request, arg_buff);
|
||||
|
||||
// we probably lowballed the payload size a bit, so added a but too much. Fix this now.
|
||||
size_t remove = arg_buff.size() % granularity;
|
||||
if (remove > request._.size())
|
||||
request._.clear();
|
||||
else
|
||||
request._.resize(request._.size() - remove);
|
||||
// if the size of _ moved enough, we might lose byte in size encoding, we don't care
|
||||
}
|
||||
|
||||
std::string fullBlob;
|
||||
if (!epee::serialization::store_t_to_binary(request, fullBlob))
|
||||
throw std::runtime_error{"Failed to serialize to epee binary format"};
|
||||
|
||||
return fullBlob;
|
||||
}
|
||||
|
||||
/* The current design uses `asio::strand`s. The documentation isn't as clear
|
||||
as it should be - a `strand` has an internal `mutex` and `bool`. The
|
||||
`mutex` synchronizes thread access and the `bool` is set when a thread is
|
||||
executing something "in the strand". Therefore, if a callback has lots of
|
||||
work to do in a `strand`, asio can switch to some other task instead of
|
||||
blocking 1+ threads to wait for the original thread to complete the task
|
||||
(as is the case when client code has a `mutex` inside the callback). The
|
||||
downside is that asio _always_ allocates for the callback, even if it can
|
||||
be immediately executed. So if all work in a strand is minimal, a lock
|
||||
may be better.
|
||||
|
||||
This code uses a strand per "zone" and a strand per "channel in a zone".
|
||||
`dispatch` is used heavily, which means "execute immediately in _this_
|
||||
thread if the strand is not in use, otherwise queue the callback to be
|
||||
executed immediately after the strand completes its current task".
|
||||
`post` is used where deferred execution to an `asio::io_service::run`
|
||||
thread is preferred.
|
||||
|
||||
The strand per "zone" is useful because the levin
|
||||
`foreach_connection` is blocked with a mutex anyway. So this primarily
|
||||
helps with reducing blocking of a thread attempting a "flood"
|
||||
notification. Updating/merging the outgoing connections in the
|
||||
Dandelion++ map is also somewhat expensive.
|
||||
|
||||
The strand per "channel" may need a re-visit. The most "expensive" code
|
||||
is figuring out the noise/notification to send. If levin code is
|
||||
optimized further, it might be better to just use standard locks per
|
||||
channel. */
|
||||
|
||||
//! A queue of levin messages for a noise i2p/tor link
|
||||
struct noise_channel
|
||||
{
|
||||
explicit noise_channel(boost::asio::io_service& io_service)
|
||||
: active(nullptr),
|
||||
queue(),
|
||||
strand(io_service),
|
||||
next_noise(io_service),
|
||||
connection(boost::uuids::nil_uuid())
|
||||
{}
|
||||
|
||||
// `asio::io_service::strand` cannot be copied or moved
|
||||
noise_channel(const noise_channel&) = delete;
|
||||
noise_channel& operator=(const noise_channel&) = delete;
|
||||
|
||||
// Only read/write these values "inside the strand"
|
||||
|
||||
epee::byte_slice active;
|
||||
std::deque<epee::byte_slice> queue;
|
||||
boost::asio::io_service::strand strand;
|
||||
boost::asio::steady_timer next_noise;
|
||||
boost::uuids::uuid connection;
|
||||
};
|
||||
} // anonymous
|
||||
|
||||
namespace detail
|
||||
{
|
||||
struct zone
|
||||
{
|
||||
explicit zone(boost::asio::io_service& io_service, std::shared_ptr<connections> p2p, epee::byte_slice noise_in)
|
||||
: p2p(std::move(p2p)),
|
||||
noise(std::move(noise_in)),
|
||||
next_epoch(io_service),
|
||||
strand(io_service),
|
||||
map(),
|
||||
channels(),
|
||||
connection_count(0)
|
||||
{
|
||||
for (std::size_t count = 0; !noise.empty() && count < CRYPTONOTE_NOISE_CHANNELS; ++count)
|
||||
channels.emplace_back(io_service);
|
||||
}
|
||||
|
||||
const std::shared_ptr<connections> p2p;
|
||||
const epee::byte_slice noise; //!< `!empty()` means zone is using noise channels
|
||||
boost::asio::steady_timer next_epoch;
|
||||
boost::asio::io_service::strand strand;
|
||||
net::dandelionpp::connection_map map;//!< Tracks outgoing uuid's for noise channels or Dandelion++ stems
|
||||
std::deque<noise_channel> channels; //!< Never touch after init; only update elements on `noise_channel.strand`
|
||||
std::atomic<std::size_t> connection_count; //!< Only update in strand, can be read at any time
|
||||
};
|
||||
} // detail
|
||||
|
||||
namespace
|
||||
{
|
||||
//! Adds a message to the sending queue of the channel.
|
||||
class queue_covert_notify
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
epee::byte_slice message_; // Requires manual copy constructor
|
||||
const std::size_t destination_;
|
||||
|
||||
public:
|
||||
queue_covert_notify(std::shared_ptr<detail::zone> zone, epee::byte_slice message, std::size_t destination)
|
||||
: zone_(std::move(zone)), message_(std::move(message)), destination_(destination)
|
||||
{}
|
||||
|
||||
queue_covert_notify(queue_covert_notify&&) = default;
|
||||
queue_covert_notify(const queue_covert_notify& source)
|
||||
: zone_(source.zone_), message_(source.message_.clone()), destination_(source.destination_)
|
||||
{}
|
||||
|
||||
//! \pre Called within `zone_->channels[destionation_].strand`.
|
||||
void operator()()
|
||||
{
|
||||
if (!zone_)
|
||||
return;
|
||||
|
||||
noise_channel& channel = zone_->channels.at(destination_);
|
||||
assert(channel.strand.running_in_this_thread());
|
||||
|
||||
if (!channel.connection.is_nil())
|
||||
channel.queue.push_back(std::move(message_));
|
||||
}
|
||||
};
|
||||
|
||||
//! Sends a message to every active connection
|
||||
class flood_notify
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
epee::byte_slice message_; // Requires manual copy
|
||||
boost::uuids::uuid source_;
|
||||
|
||||
public:
|
||||
explicit flood_notify(std::shared_ptr<detail::zone> zone, epee::byte_slice message, const boost::uuids::uuid& source)
|
||||
: zone_(std::move(zone)), message_(message.clone()), source_(source)
|
||||
{}
|
||||
|
||||
flood_notify(flood_notify&&) = default;
|
||||
flood_notify(const flood_notify& source)
|
||||
: zone_(source.zone_), message_(source.message_.clone()), source_(source.source_)
|
||||
{}
|
||||
|
||||
void operator()() const
|
||||
{
|
||||
if (!zone_ || !zone_->p2p)
|
||||
return;
|
||||
|
||||
assert(zone_->strand.running_in_this_thread());
|
||||
|
||||
/* The foreach should be quick, but then it iterates and acquires the
|
||||
same lock for every connection. So do in a strand because two threads
|
||||
will ping-pong each other with cacheline invalidations. Revisit if
|
||||
algorithm changes or the locking strategy within the levin config
|
||||
class changes. */
|
||||
|
||||
std::vector<boost::uuids::uuid> connections;
|
||||
connections.reserve(connection_id_reserve_size);
|
||||
zone_->p2p->foreach_connection([this, &connections] (detail::p2p_context& context) {
|
||||
if (this->source_ != context.m_connection_id)
|
||||
connections.emplace_back(context.m_connection_id);
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const boost::uuids::uuid& connection : connections)
|
||||
zone_->p2p->send(message_.clone(), connection);
|
||||
}
|
||||
};
|
||||
|
||||
//! Updates the connection for a channel.
|
||||
struct update_channel
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
const std::size_t channel_;
|
||||
const boost::uuids::uuid connection_;
|
||||
|
||||
//! \pre Called within `stem_.strand`.
|
||||
void operator()() const
|
||||
{
|
||||
if (!zone_)
|
||||
return;
|
||||
|
||||
noise_channel& channel = zone_->channels.at(channel_);
|
||||
assert(channel.strand.running_in_this_thread());
|
||||
static_assert(
|
||||
CRYPTONOTE_MAX_FRAGMENTS <= (noise_min_epoch / (noise_min_delay + noise_delay_range)),
|
||||
"Max fragments more than the max that can be sent in an epoch"
|
||||
);
|
||||
|
||||
/* This clears the active message so that a message "in-flight" is
|
||||
restarted. DO NOT try to send the remainder of the fragments, this
|
||||
additional send time can leak that this node was sending out a real
|
||||
notify (tx) instead of dummy noise. */
|
||||
|
||||
channel.connection = connection_;
|
||||
channel.active = nullptr;
|
||||
|
||||
if (connection_.is_nil())
|
||||
channel.queue.clear();
|
||||
}
|
||||
};
|
||||
|
||||
//! Merges `out_connections_` into the existing `zone_->map`.
|
||||
struct update_channels
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
std::vector<boost::uuids::uuid> out_connections_;
|
||||
|
||||
//! \pre Called within `zone->strand`.
|
||||
static void post(std::shared_ptr<detail::zone> zone)
|
||||
{
|
||||
if (!zone)
|
||||
return;
|
||||
|
||||
assert(zone->strand.running_in_this_thread());
|
||||
|
||||
zone->connection_count = zone->map.size();
|
||||
for (auto id = zone->map.begin(); id != zone->map.end(); ++id)
|
||||
{
|
||||
const std::size_t i = id - zone->map.begin();
|
||||
zone->channels[i].strand.post(update_channel{zone, i, *id});
|
||||
}
|
||||
}
|
||||
|
||||
//! \pre Called within `zone_->strand`.
|
||||
void operator()()
|
||||
{
|
||||
if (!zone_)
|
||||
return;
|
||||
|
||||
assert(zone_->strand.running_in_this_thread());
|
||||
if (zone_->map.update(std::move(out_connections_)))
|
||||
post(std::move(zone_));
|
||||
}
|
||||
};
|
||||
|
||||
//! Swaps out noise channels entirely; new epoch start.
|
||||
class change_channels
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
net::dandelionpp::connection_map map_; // Requires manual copy constructor
|
||||
|
||||
public:
|
||||
explicit change_channels(std::shared_ptr<detail::zone> zone, net::dandelionpp::connection_map map)
|
||||
: zone_(std::move(zone)), map_(std::move(map))
|
||||
{}
|
||||
|
||||
change_channels(change_channels&&) = default;
|
||||
change_channels(const change_channels& source)
|
||||
: zone_(source.zone_), map_(source.map_.clone())
|
||||
{}
|
||||
|
||||
//! \pre Called within `zone_->strand`.
|
||||
void operator()()
|
||||
{
|
||||
if (!zone_)
|
||||
return
|
||||
|
||||
assert(zone_->strand.running_in_this_thread());
|
||||
|
||||
zone_->map = std::move(map_);
|
||||
update_channels::post(std::move(zone_));
|
||||
}
|
||||
};
|
||||
|
||||
//! Sends a noise packet or real notification and sets timer for next call.
|
||||
struct send_noise
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
const std::size_t channel_;
|
||||
|
||||
static void wait(const std::chrono::steady_clock::time_point start, std::shared_ptr<detail::zone> zone, const std::size_t index)
|
||||
{
|
||||
if (!zone)
|
||||
return;
|
||||
|
||||
noise_channel& channel = zone->channels.at(index);
|
||||
channel.next_noise.expires_at(start + noise_min_delay + random_duration(noise_delay_range));
|
||||
channel.next_noise.async_wait(
|
||||
channel.strand.wrap(send_noise{std::move(zone), index})
|
||||
);
|
||||
}
|
||||
|
||||
//! \pre Called within `zone_->channels[channel_].strand`.
|
||||
void operator()(boost::system::error_code error)
|
||||
{
|
||||
if (!zone_ || !zone_->p2p || zone_->noise.empty())
|
||||
return;
|
||||
|
||||
if (error && error != boost::system::errc::operation_canceled)
|
||||
throw boost::system::system_error{error, "send_noise timer failed"};
|
||||
|
||||
assert(zone_->channels.at(channel_).strand.running_in_this_thread());
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
noise_channel& channel = zone_->channels.at(channel_);
|
||||
|
||||
if (!channel.connection.is_nil())
|
||||
{
|
||||
epee::byte_slice message = nullptr;
|
||||
if (!channel.active.empty())
|
||||
message = channel.active.take_slice(zone_->noise.size());
|
||||
else if (!channel.queue.empty())
|
||||
{
|
||||
channel.active = channel.queue.front().clone();
|
||||
message = channel.active.take_slice(zone_->noise.size());
|
||||
}
|
||||
else
|
||||
message = zone_->noise.clone();
|
||||
|
||||
if (zone_->p2p->send(std::move(message), channel.connection))
|
||||
{
|
||||
if (!channel.queue.empty() && channel.active.empty())
|
||||
channel.queue.pop_front();
|
||||
}
|
||||
else
|
||||
{
|
||||
channel.active = nullptr;
|
||||
channel.connection = boost::uuids::nil_uuid();
|
||||
zone_->strand.post(
|
||||
update_channels{zone_, get_out_connections(*zone_->p2p)}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wait(start, std::move(zone_), channel_);
|
||||
}
|
||||
};
|
||||
|
||||
//! Prepares connections for new channel epoch and sets timer for next epoch
|
||||
struct start_epoch
|
||||
{
|
||||
// Variables allow for Dandelion++ extension
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
std::chrono::seconds min_epoch_;
|
||||
std::chrono::seconds epoch_range_;
|
||||
std::size_t count_;
|
||||
|
||||
//! \pre Should not be invoked within any strand to prevent blocking.
|
||||
void operator()(const boost::system::error_code error = {})
|
||||
{
|
||||
if (!zone_ || !zone_->p2p)
|
||||
return;
|
||||
|
||||
if (error && error != boost::system::errc::operation_canceled)
|
||||
throw boost::system::system_error{error, "start_epoch timer failed"};
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
zone_->strand.dispatch(
|
||||
change_channels{zone_, net::dandelionpp::connection_map{get_out_connections(*(zone_->p2p)), count_}}
|
||||
);
|
||||
|
||||
detail::zone& alias = *zone_;
|
||||
alias.next_epoch.expires_at(start + min_epoch_ + random_duration(epoch_range_));
|
||||
alias.next_epoch.async_wait(start_epoch{std::move(*this)});
|
||||
}
|
||||
};
|
||||
} // anonymous
|
||||
|
||||
notify::notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise)
|
||||
: zone_(std::make_shared<detail::zone>(service, std::move(p2p), std::move(noise)))
|
||||
{
|
||||
if (!zone_->p2p)
|
||||
throw std::logic_error{"cryptonote::levin::notify cannot have nullptr p2p argument"};
|
||||
|
||||
if (!zone_->noise.empty())
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
start_epoch{zone_, noise_min_epoch, noise_epoch_range, CRYPTONOTE_NOISE_CHANNELS}();
|
||||
for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel)
|
||||
send_noise::wait(now, zone_, channel);
|
||||
}
|
||||
}
|
||||
|
||||
notify::~notify() noexcept
|
||||
{}
|
||||
|
||||
notify::status notify::get_status() const noexcept
|
||||
{
|
||||
if (!zone_)
|
||||
return {false, false};
|
||||
|
||||
return {!zone_->noise.empty(), CRYPTONOTE_NOISE_CHANNELS <= zone_->connection_count};
|
||||
}
|
||||
|
||||
void notify::new_out_connection()
|
||||
{
|
||||
if (!zone_ || zone_->noise.empty() || CRYPTONOTE_NOISE_CHANNELS <= zone_->connection_count)
|
||||
return;
|
||||
|
||||
zone_->strand.dispatch(
|
||||
update_channels{zone_, get_out_connections(*(zone_->p2p))}
|
||||
);
|
||||
}
|
||||
|
||||
void notify::run_epoch()
|
||||
{
|
||||
if (!zone_)
|
||||
return;
|
||||
zone_->next_epoch.cancel();
|
||||
}
|
||||
|
||||
void notify::run_stems()
|
||||
{
|
||||
if (!zone_)
|
||||
return;
|
||||
|
||||
for (noise_channel& channel : zone_->channels)
|
||||
channel.next_noise.cancel();
|
||||
}
|
||||
|
||||
bool notify::send_txs(std::vector<cryptonote::blobdata> txs, const boost::uuids::uuid& source, const bool pad_txs)
|
||||
{
|
||||
if (!zone_)
|
||||
return false;
|
||||
|
||||
if (!zone_->noise.empty() && !zone_->channels.empty())
|
||||
{
|
||||
// covert send in "noise" channel
|
||||
static_assert(
|
||||
CRYPTONOTE_MAX_FRAGMENTS * CRYPTONOTE_NOISE_BYTES <= LEVIN_DEFAULT_MAX_PACKET_SIZE, "most nodes will reject this fragment setting"
|
||||
);
|
||||
|
||||
// padding is not useful when using noise mode
|
||||
const std::string payload = make_tx_payload(std::move(txs), false);
|
||||
epee::byte_slice message = epee::levin::make_fragmented_notify(
|
||||
zone_->noise, NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload)
|
||||
);
|
||||
if (CRYPTONOTE_MAX_FRAGMENTS * zone_->noise.size() < message.size())
|
||||
{
|
||||
MERROR("notify::send_txs provided message exceeding covert fragment size");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t channel = 0; channel < zone_->channels.size(); ++channel)
|
||||
{
|
||||
zone_->channels[channel].strand.dispatch(
|
||||
queue_covert_notify{zone_, message.clone(), channel}
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string payload = make_tx_payload(std::move(txs), pad_txs);
|
||||
epee::byte_slice message =
|
||||
epee::levin::make_notify(NOTIFY_NEW_TRANSACTIONS::ID, epee::strspan<std::uint8_t>(payload));
|
||||
|
||||
// traditional monero send technique
|
||||
zone_->strand.dispatch(flood_notify{zone_, std::move(message), source});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // levin
|
||||
} // net
|
132
src/cryptonote_protocol/levin_notify.h
Normal file
132
src/cryptonote_protocol/levin_notify.h
Normal file
|
@ -0,0 +1,132 @@
|
|||
// Copyright (c) 2019, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "byte_slice.h"
|
||||
#include "cryptonote_basic/blobdatatype.h"
|
||||
#include "net/enums.h"
|
||||
#include "span.h"
|
||||
|
||||
namespace epee
|
||||
{
|
||||
namespace levin
|
||||
{
|
||||
template<typename> class async_protocol_handler_config;
|
||||
}
|
||||
}
|
||||
|
||||
namespace nodetool
|
||||
{
|
||||
template<typename> struct p2p_connection_context_t;
|
||||
}
|
||||
|
||||
namespace cryptonote
|
||||
{
|
||||
struct cryptonote_connection_context;
|
||||
}
|
||||
|
||||
namespace cryptonote
|
||||
{
|
||||
namespace levin
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
using p2p_context = nodetool::p2p_connection_context_t<cryptonote::cryptonote_connection_context>;
|
||||
struct zone; //!< Internal data needed for zone notifications
|
||||
} // detail
|
||||
|
||||
using connections = epee::levin::async_protocol_handler_config<detail::p2p_context>;
|
||||
|
||||
//! Provides tx notification privacy
|
||||
class notify
|
||||
{
|
||||
std::shared_ptr<detail::zone> zone_;
|
||||
|
||||
public:
|
||||
struct status
|
||||
{
|
||||
bool has_noise;
|
||||
bool connections_filled;
|
||||
};
|
||||
|
||||
//! Construct an instance that cannot notify.
|
||||
notify() noexcept
|
||||
: zone_(nullptr)
|
||||
{}
|
||||
|
||||
//! Construct an instance with available notification `zones`.
|
||||
explicit notify(boost::asio::io_service& service, std::shared_ptr<connections> p2p, epee::byte_slice noise);
|
||||
|
||||
notify(const notify&) = delete;
|
||||
notify(notify&&) = default;
|
||||
|
||||
~notify() noexcept;
|
||||
|
||||
notify& operator=(const notify&) = delete;
|
||||
notify& operator=(notify&&) = default;
|
||||
|
||||
//! \return Status information for zone selection.
|
||||
status get_status() const noexcept;
|
||||
|
||||
//! Probe for new outbound connection - skips if not needed.
|
||||
void new_out_connection();
|
||||
|
||||
//! Run the logic for the next epoch immediately. Only use in testing.
|
||||
void run_epoch();
|
||||
|
||||
//! Run the logic for the next stem timeout imemdiately. Only use in testing.
|
||||
void run_stems();
|
||||
|
||||
/*! Send txs using `cryptonote_protocol_defs.h` payload format wrapped in a
|
||||
levin header. The message will be sent in a "discreet" manner if
|
||||
enabled - if `!noise.empty()` then the `command`/`payload` will be
|
||||
queued to send at the next available noise interval. Otherwise, a
|
||||
standard Monero flood notification will be used.
|
||||
|
||||
\note Eventually Dandelion++ stem sending will be used here when
|
||||
enabled.
|
||||
|
||||
\param txs The transactions that need to be serialized and relayed.
|
||||
\param source The source of the notification. `is_nil()` indicates this
|
||||
node is the source. Dandelion++ will use this to map a source to a
|
||||
particular stem.
|
||||
\param pad_txs A request to pad txs to help conceal origin via
|
||||
statistical analysis. Ignored if noise was enabled during
|
||||
construction.
|
||||
|
||||
\return True iff the notification is queued for sending. */
|
||||
bool send_txs(std::vector<blobdata> txs, const boost::uuids::uuid& source, bool pad_txs);
|
||||
};
|
||||
} // levin
|
||||
} // net
|
|
@ -26,8 +26,8 @@
|
|||
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
set(net_sources error.cpp i2p_address.cpp parse.cpp socks.cpp socks_connect.cpp tor_address.cpp)
|
||||
set(net_headers error.h i2p_address.h parse.h socks.h socks_connect.h tor_address.h)
|
||||
set(net_sources dandelionpp.cpp error.cpp i2p_address.cpp parse.cpp socks.cpp socks_connect.cpp tor_address.cpp)
|
||||
set(net_headers dandelionpp.h error.h i2p_address.h parse.h socks.h socks_connect.h tor_address.h)
|
||||
|
||||
monero_add_library(net ${net_sources} ${net_headers})
|
||||
target_link_libraries(net common epee ${Boost_ASIO_LIBRARY})
|
||||
|
|
212
src/net/dandelionpp.cpp
Normal file
212
src/net/dandelionpp.cpp
Normal file
|
@ -0,0 +1,212 @@
|
|||
// Copyright (c) 2019, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#include "dandelionpp.h"
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#include <boost/uuid/nil_generator.hpp>
|
||||
#include <chrono>
|
||||
|
||||
#include "common/expect.h"
|
||||
#include "cryptonote_config.h"
|
||||
#include "crypto/crypto.h"
|
||||
|
||||
namespace net
|
||||
{
|
||||
namespace dandelionpp
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr const std::size_t expected_max_channels = CRYPTONOTE_NOISE_CHANNELS;
|
||||
|
||||
// could be in util somewhere
|
||||
struct key_less
|
||||
{
|
||||
template<typename K, typename V>
|
||||
bool operator()(const std::pair<K, V>& left, const K& right) const
|
||||
{
|
||||
return left.first < right;
|
||||
}
|
||||
|
||||
template<typename K, typename V>
|
||||
bool operator()(const K& left, const std::pair<K, V>& right) const
|
||||
{
|
||||
return left < right.first;
|
||||
}
|
||||
};
|
||||
|
||||
std::size_t select_stem(epee::span<const std::size_t> usage, epee::span<const boost::uuids::uuid> out_map)
|
||||
{
|
||||
assert(usage.size() < std::numeric_limits<std::size_t>::max()); // prevented in constructor
|
||||
if (usage.size() < out_map.size())
|
||||
return std::numeric_limits<std::size_t>::max();
|
||||
|
||||
// small_vector uses stack space if `expected_max_channels < capacity()`
|
||||
std::size_t lowest = std::numeric_limits<std::size_t>::max();
|
||||
boost::container::small_vector<std::size_t, expected_max_channels> choices;
|
||||
static_assert(sizeof(choices) < 256, "choices is too large based on current configuration");
|
||||
|
||||
for (const boost::uuids::uuid& out : out_map)
|
||||
{
|
||||
if (!out.is_nil())
|
||||
{
|
||||
const std::size_t location = std::addressof(out) - out_map.begin();
|
||||
if (usage[location] < lowest)
|
||||
{
|
||||
lowest = usage[location];
|
||||
choices = {location};
|
||||
}
|
||||
else if (usage[location] == lowest)
|
||||
choices.push_back(location);
|
||||
}
|
||||
}
|
||||
|
||||
switch (choices.size())
|
||||
{
|
||||
case 0:
|
||||
return std::numeric_limits<std::size_t>::max();
|
||||
case 1:
|
||||
return choices[0];
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return choices[crypto::rand_idx(choices.size())];
|
||||
}
|
||||
} // anonymous
|
||||
|
||||
connection_map::connection_map(std::vector<boost::uuids::uuid> out_connections, const std::size_t stems)
|
||||
: out_mapping_(std::move(out_connections)),
|
||||
in_mapping_(),
|
||||
usage_count_()
|
||||
{
|
||||
// max value is used by `select_stem` as error case
|
||||
if (stems == std::numeric_limits<std::size_t>::max())
|
||||
MONERO_THROW(common_error::kInvalidArgument, "stems value cannot be max size_t");
|
||||
|
||||
usage_count_.resize(stems);
|
||||
if (stems < out_mapping_.size())
|
||||
{
|
||||
for (unsigned i = 0; i < stems; ++i)
|
||||
std::swap(out_mapping_[i], out_mapping_.at(i + crypto::rand_idx(out_mapping_.size() - i)));
|
||||
|
||||
out_mapping_.resize(stems);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::shuffle(out_mapping_.begin(), out_mapping_.end(), crypto::random_device{});
|
||||
}
|
||||
}
|
||||
|
||||
connection_map::~connection_map() noexcept
|
||||
{}
|
||||
|
||||
connection_map connection_map::clone() const
|
||||
{
|
||||
return {*this};
|
||||
}
|
||||
|
||||
bool connection_map::update(std::vector<boost::uuids::uuid> current)
|
||||
{
|
||||
std::sort(current.begin(), current.end());
|
||||
|
||||
bool replace = false;
|
||||
for (auto& existing_out : out_mapping_)
|
||||
{
|
||||
const auto elem = std::lower_bound(current.begin(), current.end(), existing_out);
|
||||
if (elem == current.end() || *elem != existing_out)
|
||||
{
|
||||
existing_out = boost::uuids::nil_uuid();
|
||||
replace = true;
|
||||
}
|
||||
else // already using connection, remove it from candidate list
|
||||
current.erase(elem);
|
||||
}
|
||||
|
||||
if (!replace && out_mapping_.size() == usage_count_.size())
|
||||
return false;
|
||||
|
||||
const std::size_t existing_outs = out_mapping_.size();
|
||||
for (std::size_t i = 0; i < usage_count_.size() && !current.empty(); ++i)
|
||||
{
|
||||
const bool increase_stems = out_mapping_.size() <= i;
|
||||
if (increase_stems || out_mapping_[i].is_nil())
|
||||
{
|
||||
std::swap(current.back(), current.at(crypto::rand_idx(current.size())));
|
||||
if (increase_stems)
|
||||
out_mapping_.push_back(current.back());
|
||||
else
|
||||
out_mapping_[i] = current.back();
|
||||
current.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
return replace || existing_outs < out_mapping_.size();
|
||||
}
|
||||
|
||||
std::size_t connection_map::size() const noexcept
|
||||
{
|
||||
std::size_t count = 0;
|
||||
for (const boost::uuids::uuid& connection : out_mapping_)
|
||||
{
|
||||
if (!connection.is_nil())
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
boost::uuids::uuid connection_map::get_stem(const boost::uuids::uuid& source)
|
||||
{
|
||||
auto elem = std::lower_bound(in_mapping_.begin(), in_mapping_.end(), source, key_less{});
|
||||
if (elem == in_mapping_.end() || elem->first != source)
|
||||
{
|
||||
const std::size_t index = select_stem(epee::to_span(usage_count_), epee::to_span(out_mapping_));
|
||||
if (out_mapping_.size() < index)
|
||||
return boost::uuids::nil_uuid();
|
||||
|
||||
elem = in_mapping_.emplace(elem, source, index);
|
||||
usage_count_[index]++;
|
||||
}
|
||||
else if (out_mapping_.at(elem->second).is_nil()) // stem connection disconnected after mapping
|
||||
{
|
||||
usage_count_.at(elem->second)--;
|
||||
const std::size_t index = select_stem(epee::to_span(usage_count_), epee::to_span(out_mapping_));
|
||||
if (out_mapping_.size() < index)
|
||||
{
|
||||
in_mapping_.erase(elem);
|
||||
return boost::uuids::nil_uuid();
|
||||
}
|
||||
|
||||
elem->second = index;
|
||||
usage_count_[index]++;
|
||||
}
|
||||
|
||||
return out_mapping_[elem->second];
|
||||
}
|
||||
} // dandelionpp
|
||||
} // net
|
106
src/net/dandelionpp.h
Normal file
106
src/net/dandelionpp.h
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Copyright (c) 2019, The Monero Project
|
||||
//
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without modification, are
|
||||
// permitted provided that the following conditions are met:
|
||||
//
|
||||
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
// conditions and the following disclaimer.
|
||||
//
|
||||
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
// of conditions and the following disclaimer in the documentation and/or other
|
||||
// materials provided with the distribution.
|
||||
//
|
||||
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
||||
// used to endorse or promote products derived from this software without specific
|
||||
// prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
||||
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
||||
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
||||
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "span.h"
|
||||
|
||||
namespace net
|
||||
{
|
||||
namespace dandelionpp
|
||||
{
|
||||
//! Assists with mapping source -> stem and tracking connections for stem.
|
||||
class connection_map
|
||||
{
|
||||
// Make sure to update clone method if changing members
|
||||
std::vector<boost::uuids::uuid> out_mapping_; //<! Current outgoing uuid connection at index.
|
||||
std::vector<std::pair<boost::uuids::uuid, std::size_t>> in_mapping_; //<! uuid source to an `out_mapping_` index.
|
||||
std::vector<std::size_t> usage_count_;
|
||||
|
||||
// Use clone method to prevent "hidden" copies.
|
||||
connection_map(const connection_map&) = default;
|
||||
|
||||
public:
|
||||
using value_type = boost::uuids::uuid;
|
||||
using size_type = std::vector<boost::uuids::uuid>::size_type;
|
||||
using difference_type = std::vector<boost::uuids::uuid>::difference_type;
|
||||
using reference = const boost::uuids::uuid&;
|
||||
using const_reference = reference;
|
||||
using iterator = std::vector<boost::uuids::uuid>::const_iterator;
|
||||
using const_iterator = iterator;
|
||||
|
||||
//! Initialized with zero stem connections.
|
||||
explicit connection_map()
|
||||
: connection_map(std::vector<boost::uuids::uuid>{}, 0)
|
||||
{}
|
||||
|
||||
//! Initialized with `out_connections` and `stem_count`.
|
||||
explicit connection_map(std::vector<boost::uuids::uuid> out_connections, std::size_t stems);
|
||||
|
||||
connection_map(connection_map&&) = default;
|
||||
~connection_map() noexcept;
|
||||
connection_map& operator=(connection_map&&) = default;
|
||||
connection_map& operator=(const connection_map&) = delete;
|
||||
|
||||
//! \return An exact duplicate of `this` map.
|
||||
connection_map clone() const;
|
||||
|
||||
//! \return First stem connection.
|
||||
const_iterator begin() const noexcept
|
||||
{
|
||||
return out_mapping_.begin();
|
||||
}
|
||||
|
||||
//! \return One-past the last stem connection.
|
||||
const_iterator end() const noexcept
|
||||
{
|
||||
return out_mapping_.end();
|
||||
}
|
||||
|
||||
/*! Merges in current connections with the previous set of connections.
|
||||
If a connection died, a new one will take its place in the stem or
|
||||
the stem is marked as dead.
|
||||
|
||||
\param connections Current outbound connection ids.
|
||||
\return True if any updates to `get_connections()` was made. */
|
||||
bool update(std::vector<boost::uuids::uuid> current);
|
||||
|
||||
//! \return Number of outgoing connections in use.
|
||||
std::size_t size() const noexcept;
|
||||
|
||||
//! \return Current stem mapping for `source` or `nil_uuid()` if none is possible.
|
||||
boost::uuids::uuid get_stem(const boost::uuids::uuid& source);
|
||||
};
|
||||
} // dandelionpp
|
||||
} // net
|
|
@ -144,7 +144,7 @@ namespace nodetool
|
|||
const command_line::arg_descriptor<std::vector<std::string> > arg_p2p_add_exclusive_node = {"add-exclusive-node", "Specify list of peers to connect to only."
|
||||
" If this option is given the options add-priority-node and seed-node are ignored"};
|
||||
const command_line::arg_descriptor<std::vector<std::string> > arg_p2p_seed_node = {"seed-node", "Connect to a node to retrieve peer addresses, and disconnect"};
|
||||
const command_line::arg_descriptor<std::vector<std::string> > arg_proxy = {"proxy", "<network-type>,<socks-ip:port>[,max_connections] i.e. \"tor,127.0.0.1:9050,100\""};
|
||||
const command_line::arg_descriptor<std::vector<std::string> > arg_proxy = {"proxy", "<network-type>,<socks-ip:port>[,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""};
|
||||
const command_line::arg_descriptor<std::vector<std::string> > arg_anonymous_inbound = {"anonymous-inbound", "<hidden-service-address>,<[bind-ip:]port>[,max_connections] i.e. \"x.onion,127.0.0.1:18083,100\""};
|
||||
const command_line::arg_descriptor<bool> arg_p2p_hide_my_port = {"hide-my-port", "Do not announce yourself as peerlist candidate", false, true};
|
||||
const command_line::arg_descriptor<bool> arg_no_sync = {"no-sync", "Don't synchronize the blockchain with other peers", false};
|
||||
|
@ -163,7 +163,7 @@ namespace nodetool
|
|||
|
||||
boost::optional<std::vector<proxy>> get_proxies(boost::program_options::variables_map const& vm)
|
||||
{
|
||||
namespace ip = boost::asio::ip;
|
||||
namespace ip = boost::asio::ip;
|
||||
|
||||
std::vector<proxy> proxies{};
|
||||
|
||||
|
@ -183,14 +183,25 @@ namespace nodetool
|
|||
const boost::string_ref proxy{next->begin(), next->size()};
|
||||
|
||||
++next;
|
||||
if (!next.eof())
|
||||
for (unsigned count = 0; !next.eof(); ++count, ++next)
|
||||
{
|
||||
proxies.back().max_connections = get_max_connections(*next);
|
||||
if (proxies.back().max_connections == 0)
|
||||
if (2 <= count)
|
||||
{
|
||||
MERROR("Invalid max connections given to --" << arg_proxy.name);
|
||||
MERROR("Too many ',' characters given to --" << arg_proxy.name);
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
if (boost::string_ref{next->begin(), next->size()} == "disable_noise")
|
||||
proxies.back().noise = false;
|
||||
else
|
||||
{
|
||||
proxies.back().max_connections = get_max_connections(*next);
|
||||
if (proxies.back().max_connections == 0)
|
||||
{
|
||||
MERROR("Invalid max connections given to --" << arg_proxy.name);
|
||||
return boost::none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (epee::net_utils::zone_from_string(zone))
|
||||
|
@ -214,7 +225,7 @@ namespace nodetool
|
|||
return boost::none;
|
||||
}
|
||||
proxies.back().address = ip::tcp::endpoint{ip::address_v4{boost::endian::native_to_big(ip)}, port};
|
||||
}
|
||||
}
|
||||
|
||||
return proxies;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
#include <vector>
|
||||
|
||||
#include "cryptonote_config.h"
|
||||
#include "cryptonote_protocol/levin_notify.h"
|
||||
#include "warnings.h"
|
||||
#include "net/abstract_tcp_server2.h"
|
||||
#include "net/levin_protocol_handler.h"
|
||||
|
@ -66,12 +67,14 @@ namespace nodetool
|
|||
proxy()
|
||||
: max_connections(-1),
|
||||
address(),
|
||||
zone(epee::net_utils::zone::invalid)
|
||||
zone(epee::net_utils::zone::invalid),
|
||||
noise(true)
|
||||
{}
|
||||
|
||||
std::int64_t max_connections;
|
||||
boost::asio::ip::tcp::endpoint address;
|
||||
epee::net_utils::zone zone;
|
||||
bool noise;
|
||||
};
|
||||
|
||||
struct anonymous_inbound
|
||||
|
@ -154,6 +157,7 @@ namespace nodetool
|
|||
m_bind_ipv6_address(),
|
||||
m_port(),
|
||||
m_port_ipv6(),
|
||||
m_notifier(),
|
||||
m_our_address(),
|
||||
m_peerlist(),
|
||||
m_config{},
|
||||
|
@ -172,6 +176,7 @@ namespace nodetool
|
|||
m_bind_ipv6_address(),
|
||||
m_port(),
|
||||
m_port_ipv6(),
|
||||
m_notifier(),
|
||||
m_our_address(),
|
||||
m_peerlist(),
|
||||
m_config{},
|
||||
|
@ -189,6 +194,7 @@ namespace nodetool
|
|||
std::string m_bind_ipv6_address;
|
||||
std::string m_port;
|
||||
std::string m_port_ipv6;
|
||||
cryptonote::levin::notify m_notifier;
|
||||
epee::net_utils::network_address m_our_address; // in anonymity networks
|
||||
peerlist_manager m_peerlist;
|
||||
config m_config;
|
||||
|
@ -255,7 +261,6 @@ namespace nodetool
|
|||
size_t get_public_gray_peers_count();
|
||||
void get_public_peerlist(std::vector<peerlist_entry>& gray, std::vector<peerlist_entry>& white);
|
||||
void get_peerlist(std::vector<peerlist_entry>& gray, std::vector<peerlist_entry>& white);
|
||||
size_t get_zone_count() const { return m_network_zones.size(); }
|
||||
|
||||
void change_max_out_public_peers(size_t count);
|
||||
uint32_t get_max_out_public_peers() const;
|
||||
|
@ -330,6 +335,7 @@ namespace nodetool
|
|||
virtual void callback(p2p_connection_context& context);
|
||||
//----------------- i_p2p_endpoint -------------------------------------------------------------
|
||||
virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections);
|
||||
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const bool pad_txs);
|
||||
virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context);
|
||||
virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context);
|
||||
virtual bool drop_connection(const epee::net_utils::connection_context_base& context);
|
||||
|
|
|
@ -383,6 +383,9 @@ namespace nodetool
|
|||
m_offline = command_line::get_arg(vm, cryptonote::arg_offline);
|
||||
m_use_ipv6 = command_line::get_arg(vm, arg_p2p_use_ipv6);
|
||||
m_require_ipv4 = command_line::get_arg(vm, arg_p2p_require_ipv4);
|
||||
public_zone.m_notifier = cryptonote::levin::notify{
|
||||
public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr
|
||||
};
|
||||
|
||||
if (command_line::has_arg(vm, arg_p2p_add_peer))
|
||||
{
|
||||
|
@ -462,6 +465,7 @@ namespace nodetool
|
|||
return false;
|
||||
|
||||
|
||||
epee::byte_slice noise = nullptr;
|
||||
auto proxies = get_proxies(vm);
|
||||
if (!proxies)
|
||||
return false;
|
||||
|
@ -479,6 +483,20 @@ namespace nodetool
|
|||
|
||||
if (!set_max_out_peers(zone, proxy.max_connections))
|
||||
return false;
|
||||
|
||||
epee::byte_slice this_noise = nullptr;
|
||||
if (proxy.noise)
|
||||
{
|
||||
static_assert(sizeof(epee::levin::bucket_head2) < CRYPTONOTE_NOISE_BYTES, "noise bytes too small");
|
||||
if (noise.empty())
|
||||
noise = epee::levin::make_noise_notify(CRYPTONOTE_NOISE_BYTES);
|
||||
|
||||
this_noise = noise.clone();
|
||||
}
|
||||
|
||||
zone.m_notifier = cryptonote::levin::notify{
|
||||
zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise)
|
||||
};
|
||||
}
|
||||
|
||||
for (const auto& zone : m_network_zones)
|
||||
|
@ -494,6 +512,7 @@ namespace nodetool
|
|||
if (!inbounds)
|
||||
return false;
|
||||
|
||||
const std::size_t tx_relay_zones = m_network_zones.size();
|
||||
for (auto& inbound : *inbounds)
|
||||
{
|
||||
network_zone& zone = add_zone(inbound.our_address.get_zone());
|
||||
|
@ -504,6 +523,12 @@ namespace nodetool
|
|||
return false;
|
||||
}
|
||||
|
||||
if (zone.m_connect == nullptr && tx_relay_zones <= 1)
|
||||
{
|
||||
MERROR("Listed --" << arg_anonymous_inbound.name << " without listing any --" << arg_proxy.name << ". The latter is necessary for sending origin txes over anonymity networks");
|
||||
return false;
|
||||
}
|
||||
|
||||
zone.m_bind_ip = std::move(inbound.local_ip);
|
||||
zone.m_port = std::move(inbound.local_port);
|
||||
zone.m_net_server.set_default_remote(std::move(inbound.default_remote));
|
||||
|
@ -1266,6 +1291,7 @@ namespace nodetool
|
|||
ape.first_seen = first_seen_stamp ? first_seen_stamp : time(nullptr);
|
||||
|
||||
zone.m_peerlist.append_with_peer_anchor(ape);
|
||||
zone.m_notifier.new_out_connection();
|
||||
|
||||
LOG_DEBUG_CC(*con, "CONNECTION HANDSHAKED OK.");
|
||||
return true;
|
||||
|
@ -1990,7 +2016,7 @@ namespace nodetool
|
|||
}
|
||||
if (c_id.first <= zone->first)
|
||||
break;
|
||||
|
||||
|
||||
++zone;
|
||||
}
|
||||
if (zone->first == c_id.first)
|
||||
|
@ -2000,6 +2026,61 @@ namespace nodetool
|
|||
}
|
||||
//-----------------------------------------------------------------------------------
|
||||
template<class t_payload_net_handler>
|
||||
epee::net_utils::zone node_server<t_payload_net_handler>::send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const bool pad_txs)
|
||||
{
|
||||
namespace enet = epee::net_utils;
|
||||
|
||||
const auto send = [&txs, &source, pad_txs] (std::pair<const enet::zone, network_zone>& network)
|
||||
{
|
||||
if (network.second.m_notifier.send_txs(std::move(txs), source, (pad_txs || network.first != enet::zone::public_)))
|
||||
return network.first;
|
||||
return enet::zone::invalid;
|
||||
};
|
||||
|
||||
if (m_network_zones.empty())
|
||||
return enet::zone::invalid;
|
||||
|
||||
if (origin != enet::zone::invalid)
|
||||
return send(*m_network_zones.begin()); // send all txs received via p2p over public network
|
||||
|
||||
if (m_network_zones.size() <= 2)
|
||||
return send(*m_network_zones.rbegin()); // see static asserts below; sends over anonymity network iff enabled
|
||||
|
||||
/* These checks are to ensure that i2p is highest priority if multiple
|
||||
zones are selected. Make sure to update logic if the values cannot be
|
||||
in the same relative order. `m_network_zones` must be sorted map too. */
|
||||
static_assert(std::is_same<std::underlying_type<enet::zone>::type, std::uint8_t>{}, "expected uint8_t zone");
|
||||
static_assert(unsigned(enet::zone::invalid) == 0, "invalid expected to be 0");
|
||||
static_assert(unsigned(enet::zone::public_) == 1, "public_ expected to be 1");
|
||||
static_assert(unsigned(enet::zone::i2p) == 2, "i2p expected to be 2");
|
||||
static_assert(unsigned(enet::zone::tor) == 3, "tor expected to be 3");
|
||||
|
||||
// check for anonymity networks with noise and connections
|
||||
for (auto network = ++m_network_zones.begin(); network != m_network_zones.end(); ++network)
|
||||
{
|
||||
if (enet::zone::tor < network->first)
|
||||
break; // unknown network
|
||||
|
||||
const auto status = network->second.m_notifier.get_status();
|
||||
if (status.has_noise && status.connections_filled)
|
||||
return send(*network);
|
||||
}
|
||||
|
||||
// use the anonymity network with outbound support
|
||||
for (auto network = ++m_network_zones.begin(); network != m_network_zones.end(); ++network)
|
||||
{
|
||||
if (enet::zone::tor < network->first)
|
||||
break; // unknown network
|
||||
|
||||
if (network->second.m_connect)
|
||||
return send(*network);
|
||||
}
|
||||
|
||||
// configuration should not allow this scenario
|
||||
return enet::zone::invalid;
|
||||
}
|
||||
//-----------------------------------------------------------------------------------
|
||||
template<class t_payload_net_handler>
|
||||
void node_server<t_payload_net_handler>::callback(p2p_connection_context& context)
|
||||
{
|
||||
m_payload_handler.on_callback(context);
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
#include <boost/uuid/uuid.hpp>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "cryptonote_basic/blobdatatype.h"
|
||||
#include "net/enums.h"
|
||||
#include "net/net_utils_base.h"
|
||||
#include "p2p_protocol_defs.h"
|
||||
|
||||
|
@ -46,12 +48,12 @@ namespace nodetool
|
|||
struct i_p2p_endpoint
|
||||
{
|
||||
virtual bool relay_notify_to_list(int command, const epee::span<const uint8_t> data_buff, std::vector<std::pair<epee::net_utils::zone, boost::uuids::uuid>> connections)=0;
|
||||
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const bool pad_txs)=0;
|
||||
virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context)=0;
|
||||
virtual bool invoke_notify_to_peer(int command, const epee::span<const uint8_t> req_buff, const epee::net_utils::connection_context_base& context)=0;
|
||||
virtual bool drop_connection(const epee::net_utils::connection_context_base& context)=0;
|
||||
virtual void request_callback(const epee::net_utils::connection_context_base& context)=0;
|
||||
virtual uint64_t get_public_connections_count()=0;
|
||||
virtual size_t get_zone_count() const=0;
|
||||
virtual void for_each_connection(std::function<bool(t_connection_context&, peerid_type, uint32_t)> f)=0;
|
||||
virtual bool for_connection(const boost::uuids::uuid&, std::function<bool(t_connection_context&, peerid_type, uint32_t)> f)=0;
|
||||
virtual bool block_host(const epee::net_utils::network_address &address, time_t seconds = 0)=0;
|
||||
|
@ -71,6 +73,10 @@ namespace nodetool
|
|||
{
|
||||
return false;
|
||||
}
|
||||
virtual epee::net_utils::zone send_txs(std::vector<cryptonote::blobdata> txs, const epee::net_utils::zone origin, const boost::uuids::uuid& source, const bool pad_txs)
|
||||
{
|
||||
return epee::net_utils::zone::invalid;
|
||||
}
|
||||
virtual bool invoke_command_to_peer(int command, const epee::span<const uint8_t> req_buff, std::string& resp_buff, const epee::net_utils::connection_context_base& context)
|
||||
{
|
||||
return false;
|
||||
|
@ -96,11 +102,6 @@ namespace nodetool
|
|||
return false;
|
||||
}
|
||||
|
||||
virtual size_t get_zone_count() const
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
virtual uint64_t get_public_connections_count()
|
||||
{
|
||||
return false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue