Skip to content

Commit

Permalink
UPnP existing lease and other misc enhancements (#2837)
Browse files Browse the repository at this point in the history
* UPnP existing lease and other misc enhancements

- Add timed leases (30 minutes) with automatic renewal. The periodic map check will also renew it if something went wrong
- Response from a mapping check was being misinterpreted as remaining lease time, but it returns the total lease time, not remaining (pretty useless)
- Bug: `external_port` was not being set when a lease already exists (e.g., node crashed and restarted). This means self keepalives don't include external address and port information.
- Make it more evident (with debug_assert) that `check_mapping` and `refresh_mapping` can't be used in tests
- Always log on successful mapping rather than being behind the optional `logging.upnp_details` config; otherwise on new leases, it shows the first mapping check as an error and nothing afterwards, causes confusion

* Periodically flood keepalive_self

This ensures peers are updated with new mapping details
  • Loading branch information
guilhermelawless authored and Russel committed Jul 8, 2020
1 parent 6fc0738 commit e72dd91
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 91 deletions.
52 changes: 29 additions & 23 deletions nano/node/network.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,28 +145,7 @@ void nano::network::send_keepalive (std::shared_ptr<nano::transport::channel> ch
void nano::network::send_keepalive_self (std::shared_ptr<nano::transport::channel> channel_a)
{
nano::keepalive message;
random_fill (message.peers);
// Replace part of message with node external address or listening port
message.peers[1] = nano::endpoint (boost::asio::ip::address_v6{}, 0); // For node v19 (response channels)
if (node.config.external_address != boost::asio::ip::address_v6{}.to_string () && node.config.external_port != 0)
{
message.peers[0] = nano::endpoint (boost::asio::ip::make_address_v6 (node.config.external_address), node.config.external_port);
}
else
{
auto external_address (node.port_mapping.external_address ());
if (external_address.address () != boost::asio::ip::address_v4::any ())
{
message.peers[0] = nano::endpoint (boost::asio::ip::address_v6{}, endpoint ().port ());
boost::system::error_code ec;
auto external_v6 = boost::asio::ip::make_address_v6 (external_address.address ().to_string (), ec);
message.peers[1] = nano::endpoint (external_v6, external_address.port ());
}
else
{
message.peers[0] = nano::endpoint (boost::asio::ip::address_v6{}, endpoint ().port ());
}
}
fill_keepalive_self (message.peers);
channel_a->send (message);
}

Expand Down Expand Up @@ -667,6 +646,32 @@ void nano::network::random_fill (std::array<nano::endpoint, 8> & target_a) const
}
}

void nano::network::fill_keepalive_self (std::array<nano::endpoint, 8> & target_a) const
{
random_fill (target_a);
// Replace part of message with node external address or listening port
target_a[1] = nano::endpoint (boost::asio::ip::address_v6{}, 0); // For node v19 (response channels)
if (node.config.external_address != boost::asio::ip::address_v6{}.to_string () && node.config.external_port != 0)
{
target_a[0] = nano::endpoint (boost::asio::ip::make_address_v6 (node.config.external_address), node.config.external_port);
}
else
{
auto external_address (node.port_mapping.external_address ());
if (external_address.address () != boost::asio::ip::address_v4::any ())
{
target_a[0] = nano::endpoint (boost::asio::ip::address_v6{}, port);
boost::system::error_code ec;
auto external_v6 = boost::asio::ip::make_address_v6 (external_address.address ().to_string (), ec);
target_a[1] = nano::endpoint (external_v6, external_address.port ());
}
else
{
target_a[0] = nano::endpoint (boost::asio::ip::address_v6{}, port);
}
}
}

nano::tcp_endpoint nano::network::bootstrap_peer (bool lazy_bootstrap)
{
nano::tcp_endpoint result (boost::asio::ip::address_v6::any (), 0);
Expand Down Expand Up @@ -743,7 +748,8 @@ void nano::network::ongoing_syn_cookie_cleanup ()

void nano::network::ongoing_keepalive ()
{
flood_keepalive ();
flood_keepalive (0.75f);
flood_keepalive_self (0.25f);
std::weak_ptr<nano::node> node_w (node.shared ());
node.alarm.add (std::chrono::steady_clock::now () + node.network_params.node.half_period, [node_w]() {
if (auto node_l = node_w.lock ())
Expand Down
11 changes: 9 additions & 2 deletions nano/node/network.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,17 @@ class network final
void start ();
void stop ();
void flood_message (nano::message const &, nano::buffer_drop_policy const = nano::buffer_drop_policy::limiter, float const = 1.0f);
void flood_keepalive ()
void flood_keepalive (float const scale_a = 1.0f)
{
nano::keepalive message;
random_fill (message.peers);
flood_message (message);
flood_message (message, nano::buffer_drop_policy::limiter, scale_a);
}
void flood_keepalive_self (float const scale_a = 0.5f)
{
nano::keepalive message;
fill_keepalive_self (message.peers);
flood_message (message, nano::buffer_drop_policy::limiter, scale_a);
}
void flood_vote (std::shared_ptr<nano::vote> const &, float scale);
void flood_vote_pr (std::shared_ptr<nano::vote> const &);
Expand Down Expand Up @@ -157,6 +163,7 @@ class network final
// Desired fanout for a given scale
size_t fanout (float scale = 1.0f) const;
void random_fill (std::array<nano::endpoint, 8> &) const;
void fill_keepalive_self (std::array<nano::endpoint, 8> &) const;
// Note: The minimum protocol version is used after the random selection, so number of peers can be less than expected.
std::unordered_set<std::shared_ptr<nano::transport::channel>> random_set (size_t, uint8_t = 0, bool = false) const;
// Get the next peer for attempting a tcp bootstrap connection
Expand Down
121 changes: 62 additions & 59 deletions nano/node/portmapping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

nano::port_mapping::port_mapping (nano::node & node_a) :
node (node_a),
protocols ({ { { "TCP", 0, boost::asio::ip::address_v4::any (), 0, true }, { "UDP", 0, boost::asio::ip::address_v4::any (), 0, !node_a.flags.disable_udp } } })
protocols ({ { { "TCP", boost::asio::ip::address_v4::any (), 0, true }, { "UDP", boost::asio::ip::address_v4::any (), 0, !node_a.flags.disable_udp } } })
{
}

Expand Down Expand Up @@ -74,7 +74,8 @@ nano::endpoint nano::port_mapping::external_address ()

void nano::port_mapping::refresh_mapping ()
{
if (!network_params.network.is_test_network ())
debug_assert (!network_params.network.is_test_network ());
if (on)
{
nano::lock_guard<std::mutex> guard_l (mutex);
auto node_port_l (std::to_string (node.network.endpoint ().port ()));
Expand All @@ -83,19 +84,22 @@ void nano::port_mapping::refresh_mapping ()
// We don't map the RPC port because, unless RPC authentication was added, this would almost always be a security risk
for (auto & protocol : protocols | boost::adaptors::filtered ([](auto const & p) { return p.enabled; }))
{
auto const lease_duration = std::chrono::duration_cast<std::chrono::seconds> (network_params.portmapping.lease_duration);
auto upnp_description = std::string ("Nano Node (") + network_params.network.get_current_network_as_string () + ")";
auto add_port_mapping_error_l (UPNP_AddPortMapping (upnp.urls.controlURL, upnp.data.first.servicetype, config_port_l.c_str (), node_port_l.c_str (), address.to_string ().c_str (), upnp_description.c_str (), protocol.name, nullptr, nullptr));
auto add_port_mapping_error_l (UPNP_AddPortMapping (upnp.urls.controlURL, upnp.data.first.servicetype, config_port_l.c_str (), node_port_l.c_str (), address.to_string ().c_str (), upnp_description.c_str (), protocol.name, nullptr, std::to_string (lease_duration.count ()).c_str ()));
if (node.config.logging.upnp_details_logging ())
{
node.logger.always_log (boost::str (boost::format ("UPnP %1% port mapping response: %2%") % protocol.name % add_port_mapping_error_l));
}
if (add_port_mapping_error_l == UPNPCOMMAND_SUCCESS)
{
protocol.external_port = static_cast<uint16_t> (std::atoi (config_port_l.data ()));
if (node.config.logging.upnp_details_logging ())
{
node.logger.always_log (boost::str (boost::format ("%1% mapped to %2%") % config_port_l % node_port_l));
}
node.logger.always_log (boost::str (boost::format ("UPnP %1%:%2% mapped to %3%") % protocol.external_address % config_port_l % node_port_l));

// Refresh mapping before the leasing ends
node.alarm.add (std::chrono::steady_clock::now () + lease_duration - std::chrono::seconds (10), [node_l = node.shared ()]() {
node_l->port_mapping.refresh_mapping ();
});
}
else
{
Expand All @@ -106,80 +110,75 @@ void nano::port_mapping::refresh_mapping ()
}
}

int nano::port_mapping::check_mapping ()
bool nano::port_mapping::check_mapping ()
{
int result_l (3600);
if (!network_params.network.is_test_network ())
// Long discovery time and fast setup/teardown make this impractical for testing
debug_assert (!network_params.network.is_test_network ());
bool result_l (true);
nano::lock_guard<std::mutex> guard_l (mutex);
auto node_port_l (std::to_string (node.network.endpoint ().port ()));
auto config_port_l (get_config_port (node_port_l));
for (auto & protocol : protocols | boost::adaptors::filtered ([](auto const & p) { return p.enabled; }))
{
// Long discovery time and fast setup/teardown make this impractical for testing
nano::lock_guard<std::mutex> guard_l (mutex);
auto node_port_l (std::to_string (node.network.endpoint ().port ()));
auto config_port_l (get_config_port (node_port_l));
for (auto & protocol : protocols | boost::adaptors::filtered ([](auto const & p) { return p.enabled; }))
std::array<char, 64> int_client_l;
std::array<char, 6> int_port_l;
std::array<char, 16> remaining_mapping_duration_l;
remaining_mapping_duration_l.fill (0);
auto verify_port_mapping_error_l (UPNP_GetSpecificPortMappingEntry (upnp.urls.controlURL, upnp.data.first.servicetype, config_port_l.c_str (), protocol.name, nullptr, int_client_l.data (), int_port_l.data (), nullptr, nullptr, remaining_mapping_duration_l.data ()));
if (verify_port_mapping_error_l == UPNPCOMMAND_SUCCESS)
{
std::array<char, 64> int_client_l;
std::array<char, 6> int_port_l;
std::array<char, 16> remaining_mapping_duration_l;
remaining_mapping_duration_l.fill (0);
auto verify_port_mapping_error_l (UPNP_GetSpecificPortMappingEntry (upnp.urls.controlURL, upnp.data.first.servicetype, config_port_l.c_str (), protocol.name, nullptr, int_client_l.data (), int_port_l.data (), nullptr, nullptr, remaining_mapping_duration_l.data ()));
if (verify_port_mapping_error_l == UPNPCOMMAND_SUCCESS)
{
protocol.remaining = std::atoi (remaining_mapping_duration_l.data ());
}
else
{
protocol.remaining = 0;
node.logger.always_log (boost::str (boost::format ("UPNP_GetSpecificPortMappingEntry failed %1%: %2%") % verify_port_mapping_error_l % strupnperror (verify_port_mapping_error_l)));
}
result_l = std::min (result_l, protocol.remaining);
std::array<char, 64> external_address_l;
external_address_l.fill (0);
auto external_ip_error_l (UPNP_GetExternalIPAddress (upnp.urls.controlURL, upnp.data.first.servicetype, external_address_l.data ()));
if (external_ip_error_l == UPNPCOMMAND_SUCCESS)
{
boost::system::error_code ec;
protocol.external_address = boost::asio::ip::address_v4::from_string (external_address_l.data (), ec);
}
else
{
protocol.external_address = boost::asio::ip::address_v4::any ();
node.logger.always_log (boost::str (boost::format ("UPNP_GetExternalIPAddress failed %1%: %2%") % verify_port_mapping_error_l % strupnperror (verify_port_mapping_error_l)));
}
if (node.config.logging.upnp_details_logging ())
{
node.logger.always_log (boost::str (boost::format ("UPnP %1% mapping verification response: %2%, external ip response: %3%, external ip: %4%, internal ip: %5%, remaining lease: %6%") % protocol.name % verify_port_mapping_error_l % external_ip_error_l % external_address_l.data () % address.to_string () % remaining_mapping_duration_l.data ()));
}
result_l = false;
}
else
{
node.logger.always_log (boost::str (boost::format ("UPNP_GetSpecificPortMappingEntry failed %1%: %2%") % verify_port_mapping_error_l % strupnperror (verify_port_mapping_error_l)));
}
std::array<char, 64> external_address_l;
external_address_l.fill (0);
auto external_ip_error_l (UPNP_GetExternalIPAddress (upnp.urls.controlURL, upnp.data.first.servicetype, external_address_l.data ()));
if (external_ip_error_l == UPNPCOMMAND_SUCCESS)
{
boost::system::error_code ec;
protocol.external_address = boost::asio::ip::address_v4::from_string (external_address_l.data (), ec);
protocol.external_port = static_cast<uint16_t> (std::atoi (config_port_l.data ()));
}
else
{
protocol.external_address = boost::asio::ip::address_v4::any ();
node.logger.always_log (boost::str (boost::format ("UPNP_GetExternalIPAddress failed %1%: %2%") % verify_port_mapping_error_l % strupnperror (verify_port_mapping_error_l)));
}
if (node.config.logging.upnp_details_logging ())
{
node.logger.always_log (boost::str (boost::format ("UPnP %1% mapping verification response: %2%, external ip response: %3%, external ip: %4%, internal ip: %5%, lease: %6%") % protocol.name % verify_port_mapping_error_l % external_ip_error_l % external_address_l.data () % address.to_string () % remaining_mapping_duration_l.data ()));
}
}
return result_l;
}

void nano::port_mapping::check_mapping_loop ()
{
int wait_duration_l = network_params.portmapping.check_timeout;
refresh_devices ();
if (upnp.devices != nullptr)
{
auto remaining (check_mapping ());
// If the mapping is lost, refresh it
if (remaining == 0)
if (check_mapping ())
{
// Schedules a mapping refresh just before the leasing ends
refresh_mapping ();
}
// Check for mapping health frequently
node.alarm.add (std::chrono::steady_clock::now () + network_params.portmapping.health_check_period, [node_l = node.shared ()]() {
node_l->port_mapping.check_mapping_loop ();
});
}
else
{
wait_duration_l = 300;
if (check_count < 10)
if (check_count++ < 10)
{
node.logger.always_log (boost::str (boost::format ("UPnP No IGD devices found")));
}
}
++check_count;
if (on)
{
auto node_l (node.shared ());
node.alarm.add (std::chrono::steady_clock::now () + std::chrono::seconds (wait_duration_l), [node_l]() {
// Check for new devices later
node.alarm.add (std::chrono::steady_clock::now () + std::chrono::minutes (5), [node_l = node.shared ()]() {
node_l->port_mapping.check_mapping_loop ();
});
}
Expand All @@ -197,7 +196,11 @@ void nano::port_mapping::stop ()
auto delete_error_l (UPNP_DeletePortMapping (upnp.urls.controlURL, upnp.data.first.servicetype, std::to_string (protocol.external_port).c_str (), protocol.name, address.to_string ().c_str ()));
if (delete_error_l)
{
node.logger.always_log (boost::str (boost::format ("Shutdown port mapping response: %1%") % delete_error_l));
node.logger.always_log (boost::str (boost::format ("UPnP shutdown %1% port mapping response: %2%") % protocol.name % delete_error_l));
}
else
{
node.logger.always_log (boost::str (boost::format ("UPnP shutdown %1% port mapping successful: %2%:%3%") % protocol.name % protocol.external_address % protocol.external_port));
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions nano/node/portmapping.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class mapping_protocol
public:
/** Protocol name; TPC or UDP */
char const * name;
int remaining;
boost::asio::ip::address_v4 external_address;
uint16_t external_port;
bool enabled;
Expand Down Expand Up @@ -49,9 +48,10 @@ class port_mapping
private:
/** Add port mappings for the node port (not RPC). Refresh when the lease ends. */
void refresh_mapping ();
/** Refresh occasionally in case router loses mapping */
/** Check occasionally to refresh in case router loses mapping */
void check_mapping_loop ();
int check_mapping ();
/** Returns false if mapping still exists */
bool check_mapping ();
std::string get_config_port (std::string const &);
upnp_state upnp;
nano::node & node;
Expand Down
4 changes: 2 additions & 2 deletions nano/secure/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ nano::voting_constants::voting_constants (nano::network_constants & network_cons

nano::portmapping_constants::portmapping_constants (nano::network_constants & network_constants)
{
mapping_timeout = network_constants.is_test_network () ? 53 : 3593;
check_timeout = network_constants.is_test_network () ? 17 : 53;
lease_duration = std::chrono::seconds (1787); // ~30 minutes
health_check_period = std::chrono::seconds (53);
}

nano::bootstrap_constants::bootstrap_constants (nano::network_constants & network_constants)
Expand Down
4 changes: 2 additions & 2 deletions nano/secure/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,8 @@ class portmapping_constants
public:
portmapping_constants (nano::network_constants & network_constants);
// Timeouts are primes so they infrequently happen at the same time
int mapping_timeout;
int check_timeout;
std::chrono::seconds lease_duration;
std::chrono::seconds health_check_period;
};

/** Bootstrap related constants whose value depends on the active network */
Expand Down

0 comments on commit e72dd91

Please sign in to comment.