diff --git a/ChangeLog b/ChangeLog index 3d017fe67..55b0c2196 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,4 @@ + * add support for Port Control Protocol (PCP) * deliver notification of alerts being dropped via alerts_dropped_alert * deprecated alert::progress_notification alert category, split into finer grained categories diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index 2be68889a..826e9cf7e 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -201,6 +201,8 @@ namespace aux { // time on handler allocation every time we read again. aux::handler_storage udp_handler_storage; + std::shared_ptr natpmp_mapper; + // the key is an id that is used to identify the // client with the tracker only. std::uint32_t tracker_key = 0; @@ -636,7 +638,7 @@ namespace aux { void start_ip_notifier(); void start_lsd(); - natpmp* start_natpmp(); + void start_natpmp(); upnp* start_upnp(); void stop_ip_notifier(); @@ -644,7 +646,7 @@ namespace aux { void stop_natpmp(); void stop_upnp(); - port_mapping_t add_port_mapping(portmap_protocol t, int external_port + std::vector add_port_mapping(portmap_protocol t, int external_port , int local_port); void delete_port_mapping(port_mapping_t handle); @@ -777,6 +779,8 @@ namespace aux { void on_lsd_peer(tcp::endpoint const& peer, sha1_hash const& ih) override; + void start_natpmp(aux::listen_socket_t& s); + void set_external_address(std::shared_ptr const& sock, address const& ip , ip_source_t source_type, address const& source); @@ -1136,7 +1140,6 @@ namespace aux { // this is deducted from the connect speed int m_boost_connections = 0; - std::shared_ptr m_natpmp; std::shared_ptr m_upnp; std::shared_ptr m_lsd; diff --git a/include/libtorrent/enum_net.hpp b/include/libtorrent/enum_net.hpp index 9d6a490ef..9dbad1813 100644 --- a/include/libtorrent/enum_net.hpp +++ b/include/libtorrent/enum_net.hpp @@ -94,7 +94,9 @@ namespace libtorrent { TORRENT_EXTRA_EXPORT bool in_local_network(std::vector const& net , address const& addr); - TORRENT_EXTRA_EXPORT address get_default_gateway(io_service& ios, error_code& ec); + // returns the first default gateway found if device is empty + TORRENT_EXTRA_EXPORT address get_default_gateway(io_service& ios + , string_view device, bool v6, error_code& ec); // attempt to bind socket to the device with the specified name. For systems // that don't support SO_BINDTODEVICE the socket will be bound to one of the diff --git a/include/libtorrent/error_code.hpp b/include/libtorrent/error_code.hpp index 4eeecf206..7aa769bf0 100644 --- a/include/libtorrent/error_code.hpp +++ b/include/libtorrent/error_code.hpp @@ -326,17 +326,19 @@ namespace libtorrent { // the listen socket associated with this request was closed invalid_listen_socket, + // these error codes are deprecated, NAT-PMP/PCP error codes have + // been moved to their own category // The NAT-PMP router responded with an unsupported protocol version - unsupported_protocol_version = 120, + unsupported_protocol_version TORRENT_DEPRECATED_ENUM = 120, // You are not authorized to map ports on this NAT-PMP router - natpmp_not_authorized, + natpmp_not_authorized TORRENT_DEPRECATED_ENUM, // The NAT-PMP router failed because of a network failure - network_failure, + network_failure TORRENT_DEPRECATED_ENUM, // The NAT-PMP router failed because of lack of resources - no_resources, + no_resources TORRENT_DEPRECATED_ENUM, // The NAT-PMP router failed because an unsupported opcode was sent - unsupported_opcode, + unsupported_opcode TORRENT_DEPRECATED_ENUM, diff --git a/include/libtorrent/natpmp.hpp b/include/libtorrent/natpmp.hpp index e78e87fec..296c96ac8 100644 --- a/include/libtorrent/natpmp.hpp +++ b/include/libtorrent/natpmp.hpp @@ -43,6 +43,40 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/aux_/portmap.hpp" #include "libtorrent/aux_/vector.hpp" +namespace libtorrent { + + namespace errors + { + // See RFC 6887 Section 7.4 + enum pcp_errors + { + pcp_success = 0, + pcp_unsupp_version, + pcp_not_authorized, + pcp_malformed_request, + pcp_unsupp_opcode, + pcp_unsupp_option, + pcp_malformed_option, + pcp_network_failure, + pcp_no_resources, + pcp_unsupp_protocol, + pcp_user_ex_quota, + pcp_cannot_provide_external, + pcp_address_mismatch, + pcp_excessive_remote_peers, + }; + + boost::system::error_code make_error_code(pcp_errors e); + } + + boost::system::error_category& pcp_category(); +} + +namespace boost { namespace system { + template<> struct is_error_code_enum + { static const bool value = true; }; +} } + namespace libtorrent { struct TORRENT_EXTRA_EXPORT natpmp @@ -51,7 +85,7 @@ struct TORRENT_EXTRA_EXPORT natpmp { natpmp(io_service& ios, aux::portmap_callback& cb); - void start(); + void start(address local_address, std::string device); // maps the ports, if a port is set to 0 // it will not be mapped @@ -63,6 +97,7 @@ struct TORRENT_EXTRA_EXPORT natpmp void close(); private: + static error_code from_result_code(int version, int result); std::shared_ptr self() { return shared_from_this(); } @@ -79,8 +114,30 @@ private: void disable(error_code const& ec); + enum protocol_version + { + version_natpmp = 0, + version_pcp = 2, + }; + + static char const* version_to_string(protocol_version version); + + // See RFC 6887 Section 19.2 + enum pcp_opcode + { + opcode_announce = 0, + opcode_map, + opcode_peer, + }; + struct mapping_t : aux::base_mapping { + // random identifier, used by PCP + std::array nonce; + + // only valid if the router supports PCP + address external_address; + // the local port for this mapping. If this is set // to 0, the mapping is not in use int local_port = 0; @@ -100,6 +157,8 @@ private: aux::portmap_callback& m_callback; + protocol_version m_version = version_pcp; + aux::vector m_mappings; // the endpoint to the nat router @@ -114,9 +173,12 @@ private: int m_retry_count = 0; // used to receive responses in - char m_response_buffer[16]; + // 1100 octets is the maximum size of a PCP packet + char m_response_buffer[1100]; // router external IP address + // this is only used if the router does not support PCP + // with PCP the external IP is stored with the mapping address m_external_ip; // the endpoint we received the message from diff --git a/include/libtorrent/portmap.hpp b/include/libtorrent/portmap.hpp index 3a995cfb3..9a1ddfd9f 100644 --- a/include/libtorrent/portmap.hpp +++ b/include/libtorrent/portmap.hpp @@ -40,6 +40,7 @@ namespace libtorrent { enum class portmap_transport : std::uint8_t { + // natpmp can be NAT-PMP or PCP natpmp, upnp }; diff --git a/include/libtorrent/session_handle.hpp b/include/libtorrent/session_handle.hpp index b06fe078f..fb737de14 100644 --- a/include/libtorrent/session_handle.hpp +++ b/include/libtorrent/session_handle.hpp @@ -1012,7 +1012,7 @@ namespace libtorrent { // whichever is enabled. The return value is a handle referring to the // port mapping that was just created. Pass it to delete_port_mapping() // to remove it. - port_mapping_t add_port_mapping(portmap_protocol t, int external_port, int local_port); + std::vector add_port_mapping(portmap_protocol t, int external_port, int local_port); void delete_port_mapping(port_mapping_t handle); // This option indicates if the ports are mapped using natpmp diff --git a/src/enum_net.cpp b/src/enum_net.cpp index 206cecb20..a8fccbb8b 100644 --- a/src/enum_net.cpp +++ b/src/enum_net.cpp @@ -173,6 +173,52 @@ namespace { ); } +#if TORRENT_USE_GETIPFORWARDTABLE || TORRENT_USE_NETLINK + address build_netmask(int bits, int family) + { + if (family == AF_INET) + { + using bytes_t = boost::asio::ip::address_v4::bytes_type; + bytes_t b; + std::memset(&b[0], 0xff, b.size()); + for (int i = int(b.size()) - 1; i > 0; --i) + { + if (bits < 8) + { + b[i] <<= bits; + break; + } + b[i] = 0; + bits -= 8; + } + return address_v4(b); + } +#if TORRENT_USE_IPV6 + else if (family == AF_INET6) + { + using bytes_t = boost::asio::ip::address_v6::bytes_type; + bytes_t b; + std::memset(&b[0], 0xff, b.size()); + for (int i = int(b.size()) - 1; i > 0; --i) + { + if (bits < 8) + { + b[i] <<= bits; + break; + } + b[i] = 0; + bits -= 8; + } + return address_v6(b); + } +#endif + else + { + return address(); + } + } +#endif + #if TORRENT_USE_NETLINK int read_nl_sock(int sock, span buf, std::uint32_t const seq, std::uint32_t const pid) @@ -254,6 +300,16 @@ namespace { && rt_msg->rtm_table != RT_TABLE_LOCAL)) return false; +#if TORRENT_USE_IPV6 + // make sure the defaults have the right address family + // in case the attributes are not present + if (rt_msg->rtm_family == AF_INET6) + { + rt_info->gateway = address_v6(); + rt_info->destination = address_v6(); + } +#endif + int if_index = 0; int rt_len = int(RTM_PAYLOAD(nl_hdr)); #ifdef __clang__ @@ -298,16 +354,22 @@ namespace { #pragma clang diagnostic pop #endif +#if TORRENT_USE_IPV6 + if (rt_info->gateway.is_v6() && rt_info->gateway.to_v6().is_link_local()) + { + address_v6 gateway6 = rt_info->gateway.to_v6(); + gateway6.scope_id(if_index); + rt_info->gateway = gateway6; + } +#endif + ifreq req = {}; ::if_indextoname(std::uint32_t(if_index), req.ifr_name); static_assert(sizeof(rt_info->name) >= sizeof(req.ifr_name), "ip_route::name is too small"); std::memcpy(rt_info->name, req.ifr_name, sizeof(req.ifr_name)); ::ioctl(s, ::siocgifmtu, &req); rt_info->mtu = req.ifr_mtu; -// obviously this doesn't work correctly. How do you get the netmask for a route? -// if (ioctl(s, SIOCGIFNETMASK, &req) == 0) { -// rt_info->netmask = sockaddr_to_address(&req.ifr_addr, req.ifr_addr.sa_family); -// } + rt_info->netmask = build_netmask(rt_msg->rtm_dst_len, rt_msg->rtm_family); return true; } @@ -509,52 +571,6 @@ int _System __libsocket_sysctl(int* mib, u_int namelen, void *oldp, size_t *oldl return false; } -#if TORRENT_USE_GETIPFORWARDTABLE - address build_netmask(int bits, int family) - { - if (family == AF_INET) - { - using bytes_t = boost::asio::ip::address_v4::bytes_type; - bytes_t b; - std::memset(&b[0], 0xff, b.size()); - for (int i = int(sizeof(bytes_t)) / 8 - 1; i > 0; --i) - { - if (bits < 8) - { - b[i] <<= bits; - break; - } - b[i] = 0; - bits -= 8; - } - return address_v4(b); - } -#if TORRENT_USE_IPV6 - else if (family == AF_INET6) - { - using bytes_t = boost::asio::ip::address_v6::bytes_type; - bytes_t b; - std::memset(&b[0], 0xff, b.size()); - for (int i = int(sizeof(bytes_t)) / 8 - 1; i > 0; --i) - { - if (bits < 8) - { - b[i] <<= bits; - break; - } - b[i] = 0; - bits -= 8; - } - return address_v6(b); - } -#endif - else - { - return address(); - } - } -#endif - std::vector enum_net_interfaces(io_service& ios, error_code& ec) { TORRENT_UNUSED(ios); // this may be unused depending on configuration @@ -830,11 +846,17 @@ int _System __libsocket_sysctl(int* mib, u_int namelen, void *oldp, size_t *oldl return ret; } - address get_default_gateway(io_service& ios, error_code& ec) + address get_default_gateway(io_service& ios + , string_view device, bool v6, error_code& ec) { std::vector ret = enum_routes(ios, ec); auto const i = std::find_if(ret.begin(), ret.end() - , [](ip_route const& r) { return r.destination == address(); }); + , [device,v6](ip_route const& r) + { + return r.destination.is_unspecified() + && r.destination.is_v6() == v6 + && (device.empty() || r.name == device); + }); if (i == ret.end()) return address(); return i->gateway; } @@ -1114,15 +1136,31 @@ int _System __libsocket_sysctl(int* mib, u_int namelen, void *oldp, size_t *oldl { ip_route r; r.gateway = sockaddr_to_address((const sockaddr*)&routes->Table[i].NextHop); +#if TORRENT_USE_IPV6 + // The scope_id in NextHop is always zero because that would make + // things too easy apparently + if (r.gateway.is_v6() && r.gateway.to_v6().is_link_local()) + { + address_v6 gateway6 = r.gateway.to_v6(); + gateway6.scope_id(routes->Table[i].InterfaceIndex); + r.gateway = gateway6; + } +#endif r.destination = sockaddr_to_address( (const sockaddr*)&routes->Table[i].DestinationPrefix.Prefix); - r.netmask = build_netmask(routes->Table[i].SitePrefixLength + r.netmask = build_netmask(routes->Table[i].DestinationPrefix.PrefixLength , routes->Table[i].DestinationPrefix.Prefix.si_family); MIB_IFROW ifentry; ifentry.dwIndex = routes->Table[i].InterfaceIndex; if (GetIfEntry(&ifentry) == NO_ERROR) { - wcstombs(r.name, ifentry.wszName, sizeof(r.name)); + WCHAR* name = ifentry.wszName; + // strip UNC prefix to match the names returned by enum_net_interfaces + if (wcsncmp(name, L"\\DEVICE\\TCPIP_", wcslen(L"\\DEVICE\\TCPIP_")) == 0) + { + name += wcslen(L"\\DEVICE\\TCPIP_"); + } + wcstombs(r.name, name, sizeof(r.name)); r.name[sizeof(r.name) - 1] = 0; r.mtu = ifentry.dwMtu; ret.push_back(r); diff --git a/src/natpmp.cpp b/src/natpmp.cpp index 39cf384da..fd47dc1c0 100644 --- a/src/natpmp.cpp +++ b/src/natpmp.cpp @@ -55,11 +55,81 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/io_service.hpp" #include "libtorrent/aux_/time.hpp" #include "libtorrent/debug.hpp" +#include "libtorrent/random.hpp" +#include "libtorrent/broadcast_socket.hpp" // for is_local #include "libtorrent/aux_/escape_string.hpp" #include "libtorrent/aux_/numeric_cast.hpp" namespace libtorrent { +struct TORRENT_EXPORT pcp_error_category : boost::system::error_category +{ + const char* name() const BOOST_SYSTEM_NOEXCEPT override + { return "pcp error"; } + std::string message(int ev) const override + { + static char const* msgs[] = + { + "success", + "unsupported version", + "not authorized", + "malformed request", + "unsupported opcode", + "unsupported option", + "malformed option", + "network failure", + "no resources", + "unsupported protocol", + "user exceeded quota", + "cannot provide external", + "address mismatch", + "excessive remote peers", + }; + if (ev < 0 || ev >= int(sizeof(msgs)/sizeof(msgs[0]))) + return "Unknown error"; + return msgs[ev]; + } + boost::system::error_condition default_error_condition( + int ev) const BOOST_SYSTEM_NOEXCEPT override + { return boost::system::error_condition(ev, *this); } +}; + +boost::system::error_category& pcp_category() +{ + static pcp_error_category pcp_category; + return pcp_category; +} + +namespace errors +{ + // hidden + boost::system::error_code make_error_code(pcp_errors e) + { + return boost::system::error_code(e, pcp_category()); + } +} + +error_code natpmp::from_result_code(int const version, int result) +{ + if (version == version_natpmp) + { + // a few nat-pmp result codes map to different codes + // in pcp + switch (result) + { + case 3:result = 7; break; + case 4:result = 8; break; + case 5:result = 4; break; + } + } + return errors::pcp_errors(result); +} + +char const* natpmp::version_to_string(protocol_version version) +{ + return version == version_natpmp ? "NAT-PMP" : "PCP"; +} + using namespace aux; using namespace std::placeholders; @@ -76,19 +146,77 @@ natpmp::natpmp(io_service& ios m_mappings.reserve(10); } -void natpmp::start() +void natpmp::start(address local_address, std::string device) { TORRENT_ASSERT(is_single_thread()); error_code ec; - address const gateway = get_default_gateway(m_socket.get_io_service(), ec); - if (ec) + + // assume servers support PCP and fall back to NAT-PMP + // if necessary + m_version = version_pcp; + + // PCP requires reporting the source address at the application + // layer so the socket MUST be bound to a specific address + // if the caller didn't specify one then get the first suitable + // address from the OS + if (local_address.is_unspecified()) + { + for (auto const& a : enum_net_interfaces(m_socket.get_io_service(), ec)) + { + if (a.interface_address.is_loopback()) continue; + if (a.interface_address.is_v4() != local_address.is_v4()) continue; + if (a.interface_address.is_v6() && is_local(a.interface_address)) continue; + if (!device.empty() && a.name != device) continue; + local_address = a.interface_address; + device = a.name; + break; + } + + if (local_address.is_unspecified()) + { + // if we can't get a specific address to bind to we'll have + // to fall back to NAT-PMP + // but NAT-PMP doesn't support IPv6 so if that's what is being + // requested we can't do anything + if (local_address.is_v6()) + { + if (!ec) ec = boost::asio::error::address_family_not_supported; +#ifndef TORRENT_DISABLE_LOGGING + if (should_log()) + { + log("cannot map IPv6 without a local address, %s" + , convert_from_native(ec.message()).c_str()); + } +#endif + disable(ec); + return; + } + + m_version = version_natpmp; + ec.clear(); + } + } + + // we really want a device name to get the right default gateway + // try to find one even if the listen socket isn't bound to a device + if (device.empty()) + { + device = device_for_address(local_address, m_socket.get_io_service(), ec); + // if this fails fall back to using the first default gateway in the + // routing table + ec.clear(); + } + + address const gateway = get_default_gateway(m_socket.get_io_service() + , device, local_address.is_v6(), ec); + if (ec || gateway.is_unspecified()) { #ifndef TORRENT_DISABLE_LOGGING if (should_log()) { - log("failed to find default route: %s" - , convert_from_native(ec.message()).c_str()); + log("failed to find default route for %s: %s" + , local_address.to_string().c_str(), convert_from_native(ec.message()).c_str()); } #endif disable(ec); @@ -109,13 +237,13 @@ void natpmp::start() } #endif - m_socket.open(udp::v4(), ec); + m_socket.open(local_address.is_v4() ? udp::v4() : udp::v6(), ec); if (ec) { disable(ec); return; } - m_socket.bind({address_v4::any(), 0}, ec); + m_socket.bind({local_address, 0}, ec); if (ec) { disable(ec); @@ -126,7 +254,8 @@ void natpmp::start() m_socket.async_receive_from(boost::asio::buffer(&m_response_buffer[0] , sizeof(m_response_buffer)) , m_remote, std::bind(&natpmp::on_reply, self(), _1, _2)); - send_get_ip_address_request(); + if (m_version == version_natpmp) + send_get_ip_address_request(); for (auto i = m_mappings.begin(), end(m_mappings.end()); i != end; ++i) { @@ -143,9 +272,15 @@ void natpmp::send_get_ip_address_request() TORRENT_ASSERT(is_single_thread()); using namespace libtorrent::detail; + // this opcode only exists in NAT-PMP + // PCP routers report the external IP in the response to a MAP operation + TORRENT_ASSERT(m_version == version_natpmp); + if (m_version != version_natpmp) + return; + char buf[2]; char* out = buf; - write_uint8(0, out); // NAT-PMP version + write_uint8(version_natpmp, out); write_uint8(0, out); // public IP address request opcode #ifndef TORRENT_DISABLE_LOGGING log("==> get public IP address"); @@ -256,6 +391,7 @@ port_mapping_t natpmp::add_mapping(portmap_protocol const p, int const external_ m_mappings.push_back(mapping_t()); i = m_mappings.end() - 1; } + aux::random_bytes(i->nonce); i->protocol = p; i->external_port = external_port; i->local_port = local_ep.port(); @@ -344,29 +480,85 @@ void natpmp::send_map_request(port_mapping_t const i) m_currently_mapping = i; mapping_t& m = m_mappings[i]; TORRENT_ASSERT(m.act != portmap_action::none); - char buf[12]; + char buf[60]; char* out = buf; - write_uint8(0, out); // NAT-PMP version - write_uint8(m.protocol == portmap_protocol::udp ? 1 : 2, out); // map "protocol" - write_uint16(0, out); // reserved - write_uint16(m.local_port, out); // private port - write_uint16(m.external_port, out); // requested public port int ttl = m.act == portmap_action::add ? 3600 : 0; - write_uint32(ttl, out); // port mapping lifetime + if (m_version == version_natpmp) + { + write_uint8(m_version, out); + write_uint8(m.protocol == portmap_protocol::udp ? 1 : 2, out); // map "protocol" + write_uint16(0, out); // reserved + write_uint16(m.local_port, out); // private port + write_uint16(m.external_port, out); // requested public port + write_uint32(ttl, out); // port mapping lifetime + } + else if (m_version == version_pcp) + { + // PCP requires the use of IPv6 addresses even for IPv4 messages + // reference asio's address_v6 class directly so that we can use it + // even if TORRENT_USE_IPV6 is false + using boost::asio::ip::address_v6; + write_uint8(m_version, out); + write_uint8(opcode_map, out); + write_uint16(0, out); // reserved + write_uint32(ttl, out); + address const local_addr = m_socket.local_endpoint().address(); + auto const local_bytes = local_addr.is_v4() + ? address_v6::v4_mapped(local_addr.to_v4()).to_bytes() + : local_addr.to_v6().to_bytes(); + out = std::copy(local_bytes.begin(), local_bytes.end(), out); + out = std::copy(m.nonce.begin(), m.nonce.end(), out); + // translate portmap_protocol to an IANA protocol number + int const protocol = + (m.protocol == portmap_protocol::tcp) ? 6 + : (m.protocol == portmap_protocol::udp) ? 17 + : 0; + write_int8(protocol, out); + write_uint8(0, out); // reserved + write_uint16(0, out); // reserved + write_uint16(m.local_port, out); + write_uint16(m.external_port, out); + address_v6::bytes_type external_addr; + if (!m.external_address.is_unspecified()) + { + external_addr = m.external_address.is_v4() + ? address_v6::v4_mapped(m.external_address.to_v4()).to_bytes() + : m.external_address.to_v6().to_bytes(); + } + else if (is_local(local_addr)) + { + external_addr = local_addr.is_v4() ? address_v6::v4_mapped(address_v4()).to_bytes() + : address_v6().to_bytes(); + } + else if (local_addr.is_v4()) + { + external_addr = address_v6::v4_mapped(local_addr.to_v4()).to_bytes(); + } + else + { + external_addr = local_addr.to_v6().to_bytes(); + } + out = std::copy(external_addr.begin(), external_addr.end(), out); + } + else + { + TORRENT_ASSERT_FAIL(); + } #ifndef TORRENT_DISABLE_LOGGING if (should_log()) { log("==> port map [ mapping: %d action: %s" - " proto: %s local: %u external: %u ttl: %u ]" + " transport: %s proto: %s local: %u external: %u ttl: %u ]" , static_cast(i), to_string(m.act) + , version_to_string(m_version) , to_string(m.protocol) , m.local_port, m.external_port, ttl); } #endif error_code ec; - m_socket.send_to(boost::asio::buffer(buf, sizeof(buf)), m_nat_endpoint, 0, ec); + m_socket.send_to(boost::asio::buffer(buf, std::size_t(out - buf)), m_nat_endpoint, 0, ec); m.map_sent = true; m.outstanding_request = true; if (m_abort) @@ -454,7 +646,7 @@ void natpmp::on_reply(error_code const& e error_code ec; m_send_timer.cancel(ec); - if (bytes_transferred < 12) + if (bytes_transferred < 4) { #ifndef TORRENT_DISABLE_LOGGING log("received packet of invalid size: %d", int(bytes_transferred)); @@ -464,13 +656,64 @@ void natpmp::on_reply(error_code const& e char* in = msg_buf; int const version = read_uint8(in); - int const cmd = read_uint8(in); - int const result = read_uint16(in); + + if (version != version_natpmp && version != version_pcp) + { +#ifndef TORRENT_DISABLE_LOGGING + log("unexpected version: %u", version); +#endif + return; + } + + int cmd = read_uint8(in); + if (version == version_pcp) + { + cmd &= 0x7f; + } + int result; + if (version == version_pcp) + { + ++in; // reserved + result = read_uint8(in); + } + else + { + result = read_uint16(in); + } + + if (result == errors::pcp_unsupp_version) + { +#ifndef TORRENT_DISABLE_LOGGING + log("unsupported version"); +#endif + if (m_version == version_pcp && !is_v6(m_socket.local_endpoint())) + { + m_version = version_natpmp; + resend_request(m_currently_mapping, error_code()); + send_get_ip_address_request(); + } + return; + } + + if ((version == version_natpmp && bytes_transferred < 12) + || (version == version_pcp && bytes_transferred < 24)) + { +#ifndef TORRENT_DISABLE_LOGGING + log("received packet of invalid size: %d", int(bytes_transferred)); +#endif + return; + } + + int lifetime = 0; + if (version == version_pcp) + { + lifetime = aux::numeric_cast(read_uint32(in)); + } int const time = aux::numeric_cast(read_uint32(in)); - TORRENT_UNUSED(version); + if (version == version_pcp) in += 12; // reserved TORRENT_UNUSED(time); - if (cmd == 128) + if (version == version_natpmp && cmd == 128) { // public IP request response m_external_ip = read_v4_address(in); @@ -485,7 +728,8 @@ void natpmp::on_reply(error_code const& e } - if (bytes_transferred != 16) + if ((version == version_natpmp && bytes_transferred != 16) + || (version == version_pcp && bytes_transferred != 60)) { #ifndef TORRENT_DISABLE_LOGGING log("received packet of invalid size: %d", int(bytes_transferred)); @@ -493,27 +737,47 @@ void natpmp::on_reply(error_code const& e return; } + std::array nonce; + portmap_protocol protocol = portmap_protocol::none; + if (version == version_pcp) + { + std::memcpy(nonce.data(), in, nonce.size()); + in += nonce.size(); + int p = read_uint8(in); + protocol = p == 6 ? portmap_protocol::tcp + : portmap_protocol::udp; + in += 3; // reserved + } int const private_port = read_uint16(in); int const public_port = read_uint16(in); - int const lifetime = aux::numeric_cast(read_uint32(in)); + if (version == version_natpmp) + lifetime = aux::numeric_cast(read_uint32(in)); + address external_addr; + if (version == version_pcp) + { + using boost::asio::ip::address_v6; + address_v6::bytes_type addr; + std::memcpy(addr.data(), in, addr.size()); + in += addr.size(); + external_addr = address_v6(addr); + if (external_addr.to_v6().is_v4_mapped()) + external_addr = external_addr.to_v6().to_v4(); + } - portmap_protocol const protocol = (cmd - 128 == 1) - ? portmap_protocol::udp - : portmap_protocol::tcp; + if (version == version_natpmp) + { + protocol = (cmd - 128 == 1) + ? portmap_protocol::udp + : portmap_protocol::tcp; + } #ifndef TORRENT_DISABLE_LOGGING char msg[200]; int const num_chars = std::snprintf(msg, sizeof(msg), "<== port map [" - " protocol: %s local: %u external: %u ttl: %u ]" - , (cmd - 128 == 1 ? "udp" : "tcp") + " transport: %s protocol: %s local: %u external: %u ttl: %u ]" + , version_to_string(protocol_version(version)) + , (protocol == portmap_protocol::udp ? "udp" : "tcp") , private_port, public_port, lifetime); - - if (version != 0) - { - std::snprintf(msg + num_chars, sizeof(msg) - aux::numeric_cast(num_chars), "unexpected version: %u" - , version); - log("%s", msg); - } #endif mapping_t* m = nullptr; @@ -524,6 +788,7 @@ void natpmp::on_reply(error_code const& e if (protocol != i->protocol) continue; if (!i->map_sent) continue; if (!i->outstanding_request) continue; + if (version == version_pcp && nonce != i->nonce) continue; m = &*i; index = port_mapping_t(static_cast(i - m_mappings.begin())); break; @@ -553,32 +818,23 @@ void natpmp::on_reply(error_code const& e { m->expires = aux::time_now() + seconds(int(lifetime * 0.7f)); m->external_port = public_port; + if (!external_addr.is_unspecified()) + m->external_address = external_addr; } if (result != 0) { - // TODO: 3 it would be nice to have a separate NAT-PMP error category - static errors::error_code_enum const errors[] = - { - errors::unsupported_protocol_version, - errors::natpmp_not_authorized, - errors::network_failure, - errors::no_resources, - errors::unsupported_opcode - }; - errors::error_code_enum ev = errors::no_error; - if (result >= 1 && result <= 5) ev = errors[result - 1]; - m->expires = aux::time_now() + hours(2); portmap_protocol const proto = m->protocol; m_callback.on_port_mapping(port_mapping_t{index}, address(), 0, proto - , ev, portmap_transport::natpmp); + , from_result_code(version, result), portmap_transport::natpmp); } else if (m->act == portmap_action::add) { portmap_protocol const proto = m->protocol; - m_callback.on_port_mapping(port_mapping_t{index}, m_external_ip, m->external_port, proto - , errors::no_error, portmap_transport::natpmp); + address const ext_ip = version == version_pcp ? m->external_address : m_external_ip; + m_callback.on_port_mapping(port_mapping_t{index}, ext_ip, m->external_port, proto + , errors::pcp_success, portmap_transport::natpmp); } if (m_abort) return; diff --git a/src/session_handle.cpp b/src/session_handle.cpp index bf93a5cd7..bd87aa21f 100644 --- a/src/session_handle.cpp +++ b/src/session_handle.cpp @@ -1226,10 +1226,10 @@ namespace { } #endif // TORRENT_ABI_VERSION - port_mapping_t session_handle::add_port_mapping(portmap_protocol const t + std::vector session_handle::add_port_mapping(portmap_protocol const t , int external_port, int local_port) { - return sync_call_ret(&session_impl::add_port_mapping, t, external_port, local_port); + return sync_call_ret>(&session_impl::add_port_mapping, t, external_port, local_port); } void session_handle::delete_port_mapping(port_mapping_t handle) diff --git a/src/session_impl.cpp b/src/session_impl.cpp index d85c776ad..93488a8d5 100644 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -586,7 +586,7 @@ namespace aux { // apply all m_settings to this session run_all_updates(*this); - reopen_listen_sockets(); + reopen_listen_sockets(false); reopen_outgoing_sockets(); #if TORRENT_USE_INVARIANT_CHECKS @@ -1859,6 +1859,7 @@ namespace aux { #endif if ((*remove_iter)->sock) (*remove_iter)->sock->close(ec); if ((*remove_iter)->udp_sock) (*remove_iter)->udp_sock->sock.close(); + if ((*remove_iter)->natpmp_mapper) (*remove_iter)->natpmp_mapper->close(); remove_iter = m_listen_sockets.erase(remove_iter); } @@ -1879,9 +1880,11 @@ namespace aux { TORRENT_ASSERT((s->incoming == duplex::accept_incoming) == bool(s->sock)); if (s->sock) async_accept(s->sock, s->ssl); + if (m_settings.get_bool(settings_pack::enable_natpmp)) + start_natpmp(*s); // since this is a new socket it needs to map ports // even if the caller did not request re-mapping - if (!map_ports) remap_ports(remap_natpmp_and_upnp, *s); + if (!map_ports) remap_ports(remap_upnp, *s); } } @@ -2126,10 +2129,10 @@ namespace aux { tcp::endpoint const tcp_ep = s.sock ? s.sock->local_endpoint() : tcp::endpoint(); udp::endpoint const udp_ep = s.udp_sock ? s.udp_sock->sock.local_endpoint() : udp::endpoint(); - if ((mask & remap_natpmp) && m_natpmp) + if ((mask & remap_natpmp) && s.natpmp_mapper) { - map_port(*m_natpmp, portmap_protocol::tcp, tcp_ep, s.tcp_port_mapping[0]); - map_port(*m_natpmp, portmap_protocol::udp, to_tcp(udp_ep), s.udp_port_mapping[0]); + map_port(*s.natpmp_mapper, portmap_protocol::tcp, tcp_ep, s.tcp_port_mapping[0]); + map_port(*s.natpmp_mapper, portmap_protocol::udp, to_tcp(udp_ep), s.udp_port_mapping[0]); } if ((mask & remap_upnp) && m_upnp) { @@ -5450,6 +5453,23 @@ namespace aux { m_alerts.emplace_alert(t->get_handle(), peer); } + void session_impl::start_natpmp(aux::listen_socket_t& s) + { + // don't create mappings for local IPv6 addresses + // they can't be reached from outside of the local network anyways + if (is_v6(s.local_endpoint) && is_local(s.local_endpoint.address())) + return; + + if (!s.natpmp_mapper) + { + // the natpmp constructor may fail and call the callbacks + // into the session_impl. + s.natpmp_mapper = std::make_shared(m_io_service, *this); + s.natpmp_mapper->start(s.local_endpoint.address(), s.device); + } + remap_ports(remap_natpmp, s); + } + namespace { bool find_tcp_port_mapping(portmap_transport const transport , port_mapping_t mapping, std::shared_ptr const& ls) @@ -6646,22 +6666,11 @@ namespace aux { m_alerts.emplace_alert(ec); } - natpmp* session_impl::start_natpmp() + void session_impl::start_natpmp() { INVARIANT_CHECK; - - if (m_natpmp) return m_natpmp.get(); - - // the natpmp constructor may fail and call the callbacks - // into the session_impl. - m_natpmp = std::make_shared(m_io_service, *this); - m_natpmp->start(); - for (auto& s : m_listen_sockets) - { - remap_ports(remap_natpmp, *s); - } - return m_natpmp.get(); + start_natpmp(*s); } upnp* session_impl::start_upnp() @@ -6687,22 +6696,28 @@ namespace aux { return m_upnp.get(); } - port_mapping_t session_impl::add_port_mapping(portmap_protocol const t + std::vector session_impl::add_port_mapping(portmap_protocol const t , int const external_port , int const local_port) { - port_mapping_t ret{-1}; - if (m_upnp) ret = m_upnp->add_mapping(t, external_port - , tcp::endpoint({}, static_cast(local_port))); - if (m_natpmp) ret = m_natpmp->add_mapping(t, external_port - , tcp::endpoint({}, static_cast(local_port))); + std::vector ret; + if (m_upnp) ret.push_back(m_upnp->add_mapping(t, external_port + , tcp::endpoint({}, static_cast(local_port)))); + for (auto& s : m_listen_sockets) + { + if (s->natpmp_mapper) ret.push_back(s->natpmp_mapper->add_mapping(t, external_port + , tcp::endpoint({}, static_cast(local_port)))); + } return ret; } void session_impl::delete_port_mapping(port_mapping_t handle) { if (m_upnp) m_upnp->delete_mapping(handle); - if (m_natpmp) m_natpmp->delete_mapping(handle); + for (auto& s : m_listen_sockets) + { + if (s->natpmp_mapper) s->natpmp_mapper->delete_mapping(handle); + } } void session_impl::stop_ip_notifier() @@ -6722,16 +6737,14 @@ namespace aux { void session_impl::stop_natpmp() { - if (!m_natpmp) return; - - m_natpmp->close(); for (auto& s : m_listen_sockets) { s->tcp_port_mapping[0] = port_mapping_t{-1}; s->udp_port_mapping[0] = port_mapping_t{-1}; + if (!s->natpmp_mapper) continue; + s->natpmp_mapper->close(); + s->natpmp_mapper.reset(); } - - m_natpmp.reset(); } void session_impl::stop_upnp() diff --git a/test/enum_if.cpp b/test/enum_if.cpp index a8a5dbf62..6a7d8806d 100644 --- a/test/enum_if.cpp +++ b/test/enum_if.cpp @@ -42,7 +42,7 @@ int main() io_service ios; error_code ec; - address def_gw = get_default_gateway(ios, ec); + address def_gw = get_default_gateway(ios, "", false, ec); if (ec) { std::printf("%s\n", ec.message().c_str()); @@ -80,17 +80,19 @@ int main() return 1; } - std::printf("%-34s%-45s%-20s%-20sdescription\n", "address", "netmask", "name", "flags"); + std::printf("%-34s%-45s%-20s%-20s%-34sdescription\n", "address", "netmask", "name", "flags", "default gateway"); for (auto const& i : net) { - std::printf("%-34s%-45s%-20s%s%s%-20s%s %s\n" + address iface_def_gw = get_default_gateway(ios, i.name, i.interface_address.is_v6(), ec); + std::printf("%-34s%-45s%-20s%s%s%-20s%-34s%s %s\n" , i.interface_address.to_string(ec).c_str() , i.netmask.to_string(ec).c_str() , i.name , (i.interface_address.is_multicast()?"multicast ":"") , (is_local(i.interface_address)?"local ":"") , (is_loopback(i.interface_address)?"loopback ":"") + , iface_def_gw.to_string(ec).c_str() , i.friendly_name, i.description); } }