dncurrency/nano/node/portmapping.cpp
2024-08-11 11:10:58 +02:00

370 lines
11 KiB
C++

#include <nano/lib/thread_roles.hpp>
#include <nano/node/node.hpp>
#include <nano/node/portmapping.hpp>
#include <miniupnp/miniupnpc/include/upnpcommands.h>
#include <miniupnp/miniupnpc/include/upnperrors.h>
#include <boost/range/adaptor/filtered.hpp>
std::string nano::mapping_protocol::to_string ()
{
std::stringstream ss;
ss << name << " " << external_address << ":" << external_port;
ss << (enabled ? " (enabled)" : " (disabled)");
return ss.str ();
};
/*
* port_mapping
*/
nano::port_mapping::port_mapping (nano::node & node_a) :
node (node_a),
// Kept UDP in the array (set disabled) so the port mapping is still
// implemented in case other transport protocols that rely on it is added.
protocols ({ { { "TCP", boost::asio::ip::address_v4::any (), 0, true }, { "UDP", boost::asio::ip::address_v4::any (), 0, false } } })
{
}
nano::port_mapping::~port_mapping ()
{
debug_assert (!thread.joinable ());
}
void nano::port_mapping::start ()
{
debug_assert (!thread.joinable ());
if (!node.config.enable_upnp)
{
return;
}
// Long discovery time and fast setup/teardown make this impractical for testing
// TODO: Find a way to test this
if (node.network_params.network.is_dev_network ())
{
return;
}
thread = std::thread ([this] {
nano::thread_role::set (nano::thread_role::name::port_mapping);
run ();
});
}
void nano::port_mapping::stop ()
{
{
nano::lock_guard<nano::mutex> guard (mutex);
stopped = true;
}
condition.notify_all ();
if (thread.joinable ())
{
thread.join ();
}
}
void nano::port_mapping::shutdown ()
{
node.logger.debug (nano::log::type::upnp, "UPnP shutdown...");
nano::lock_guard<nano::mutex> guard_l (mutex);
for (auto & protocol : protocols | boost::adaptors::filtered ([] (auto const & p) { return p.enabled; }))
{
if (protocol.external_port != 0)
{
std::string external_port_str = std::to_string (protocol.external_port);
std::string address_str = address.to_string ();
// Be a good citizen for the router and shut down our mapping
auto delete_error_l = UPNP_DeletePortMapping (upnp.urls.controlURL, upnp.data.first.servicetype, external_port_str.c_str (), protocol.name, address_str.c_str ());
if (delete_error_l)
{
node.logger.warn (nano::log::type::upnp, "UPnP shutdown {} port mapping failed: {} ({})",
protocol.name,
delete_error_l,
strupnperror (delete_error_l));
}
else
{
node.logger.info (nano::log::type::upnp, "UPnP shutdown {} port mapping successful: {}:{}",
protocol.name,
protocol.external_address.to_string (),
protocol.external_port);
}
}
}
}
std::string nano::port_mapping::get_config_port (std::string const & node_port_a)
{
return node.config.external_port != 0 ? std::to_string (node.config.external_port) : node_port_a;
}
std::string nano::port_mapping::to_string ()
{
std::stringstream ss;
ss << "port_mapping is " << (stopped ? "stopped" : "running") << std::endl;
for (auto & protocol : protocols)
{
ss << protocol.to_string () << std::endl;
}
ss << upnp.to_string ();
return ss.str ();
};
void nano::port_mapping::refresh_devices ()
{
upnp_state upnp_l;
int discover_error_l = 0;
upnp_l.devices = upnpDiscover (2000, nullptr, nullptr, UPNP_LOCAL_PORT_ANY, false, 2, &discover_error_l);
std::array<char, 64> local_address_l;
local_address_l.fill (0);
auto igd_error_l (UPNP_GetValidIGD (upnp_l.devices, &upnp_l.urls, &upnp_l.data, local_address_l.data (), sizeof (local_address_l)));
// Bump logging level periodically
node.logger.log ((check_count % 15 == 0) ? nano::log::level::info : nano::log::level::debug,
nano::log::type::upnp, "UPnP local address: {}, discovery: {}, IGD search: {}",
local_address_l.data (),
discover_error_l,
igd_error_l);
for (auto i (upnp_l.devices); i != nullptr; i = i->pNext)
{
node.logger.debug (nano::log::type::upnp, "UPnP device url: {}, st: {}, usn: {}", i->descURL, i->st, i->usn);
}
// Update port mapping
nano::lock_guard<nano::mutex> guard_l (mutex);
upnp = std::move (upnp_l);
if (igd_error_l == 1 || igd_error_l == 2)
{
boost::system::error_code ec;
address = boost::asio::ip::address_v4::from_string (local_address_l.data (), ec);
}
}
nano::endpoint nano::port_mapping::external_address ()
{
nano::endpoint result_l (boost::asio::ip::address_v6{}, 0);
nano::lock_guard<nano::mutex> guard_l (mutex);
for (auto & protocol : protocols | boost::adaptors::filtered ([] (auto const & p) { return p.enabled; }))
{
if (protocol.external_port != 0)
{
result_l = nano::endpoint (protocol.external_address, protocol.external_port);
}
}
return result_l;
}
void nano::port_mapping::refresh_mapping ()
{
nano::lock_guard<nano::mutex> guard_l (mutex);
if (stopped)
{
return;
}
auto node_port_l (std::to_string (node.network.endpoint ().port ()));
auto config_port_l (get_config_port (node_port_l));
// 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 upnp_description = std::string ("Nano Node (") + node.network_params.network.get_current_network_as_string () + ")";
std::string address_str = address.to_string ();
std::string lease_duration_str = std::to_string (node.network_params.portmapping.lease_duration.count ());
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_str.c_str (), upnp_description.c_str (), protocol.name, nullptr, lease_duration_str.c_str ());
if (add_port_mapping_error_l == UPNPCOMMAND_SUCCESS)
{
protocol.external_port = static_cast<uint16_t> (std::atoi (config_port_l.data ()));
node.logger.info (nano::log::type::upnp, "UPnP {} {}:{} mapped to: {}",
protocol.name,
protocol.external_address.to_string (),
config_port_l,
node_port_l);
}
else
{
protocol.external_port = 0;
node.logger.warn (nano::log::type::upnp, "UPnP {} {}:{} failed: {} ({})",
protocol.name,
protocol.external_address.to_string (),
config_port_l,
add_port_mapping_error_l,
strupnperror (add_port_mapping_error_l));
}
}
}
bool nano::port_mapping::check_lost_or_old_mapping ()
{
bool result_l (false);
nano::lock_guard<nano::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 ()));
auto remaining_from_port_mapping = std::atoi (remaining_mapping_duration_l.data ());
auto lease_duration = node.network_params.portmapping.lease_duration.count ();
auto lease_duration_divided_by_two = (lease_duration / 2);
auto recent_lease = (remaining_from_port_mapping >= lease_duration_divided_by_two);
if (verify_port_mapping_error_l != UPNPCOMMAND_SUCCESS)
{
result_l = true;
node.logger.warn (nano::log::type::upnp, "UPnP get specific port mapping failed: {} ({})",
verify_port_mapping_error_l,
strupnperror (verify_port_mapping_error_l));
}
if (!recent_lease)
{
result_l = true;
node.logger.info (nano::log::type::upnp, "UPnP lease time getting old, remaining time: {}, lease time: {}, below the threshold: {}",
remaining_from_port_mapping,
lease_duration,
lease_duration_divided_by_two);
}
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.warn (nano::log::type::upnp, "UPnP get external ip address failed: {} ({})",
external_ip_error_l,
strupnperror (external_ip_error_l));
}
node.logger.debug (nano::log::type::upnp, "UPnP {} mapping verification response: {}, external ip response: {}, external ip: {}, internal ip: {}, remaining lease: {}",
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 ()
{
debug_assert (!node.network_params.network.is_dev_network ());
refresh_devices ();
if (upnp.devices != nullptr)
{
// If the mapping is lost, refresh it
if (check_lost_or_old_mapping ())
{
// Schedules a mapping refresh just before the leasing ends
refresh_mapping ();
}
else
{
node.logger.info (nano::log::type::upnp, "UPnP no need to refresh the mapping");
}
}
else
{
// Bump logging level periodically
node.logger.log ((check_count % 15 == 0) ? nano::log::level::info : nano::log::level::debug,
nano::log::type::upnp, "UPnP no IGD devices found");
}
++check_count;
}
void nano::port_mapping::run ()
{
nano::unique_lock<nano::mutex> lock{ mutex };
while (!stopped)
{
node.stats.inc (nano::stat::type::port_mapping, nano::stat::detail::loop);
lock.unlock ();
check_mapping ();
lock.lock ();
condition.wait_for (lock, node.network_params.portmapping.health_check_period, [this] { return stopped.load (); });
}
lock.unlock ();
shutdown ();
}
/*
* upnp_state
*/
std::string nano::upnp_state::to_string ()
{
std::stringstream ss;
ss << "Discovered UPnP devices:" << std::endl;
for (UPNPDev * p = devices; p; p = p->pNext)
{
debug_assert (p->descURL);
debug_assert (p->st);
debug_assert (p->usn);
ss << " " << p->descURL << std::endl;
ss << " " << p->st << std::endl;
ss << " " << p->usn << std::endl;
}
ss << " scope_id: " << std::endl;
return ss.str ();
}
nano::upnp_state::~upnp_state ()
{
if (devices)
{
freeUPNPDevlist (devices);
}
FreeUPNPUrls (&urls);
}
nano::upnp_state & nano::upnp_state::operator= (nano::upnp_state && other_a)
{
if (this == &other_a)
{
return *this;
}
if (devices)
{
freeUPNPDevlist (devices);
}
devices = other_a.devices;
other_a.devices = nullptr;
FreeUPNPUrls (&urls);
urls = other_a.urls;
other_a.urls = { 0 };
data = other_a.data;
other_a.data = { { 0 } };
return *this;
}