From 779014ddac22e4335d7db2143f7fc8c450027273 Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Sun, 5 Dec 2010 20:40:28 +0000 Subject: [PATCH] support requesting UPnP and NAT-PMP routers for our external IP address --- docs/manual.rst | 6 + include/libtorrent/aux_/session_impl.hpp | 10 +- include/libtorrent/natpmp.hpp | 8 +- include/libtorrent/session_settings.hpp | 7 ++ include/libtorrent/upnp.hpp | 8 +- src/http_tracker_connection.cpp | 44 ++++--- src/natpmp.cpp | 58 ++++++++- src/session_impl.cpp | 22 +++- src/upnp.cpp | 144 ++++++++++++++++++++++- test/test_upnp.cpp | 4 +- 10 files changed, 276 insertions(+), 35 deletions(-) diff --git a/docs/manual.rst b/docs/manual.rst index ff8c6676a..2b3fc810c 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -3992,6 +3992,8 @@ session_settings bool rate_limit_utp; int listen_queue_size; + + bool announce_double_nat; }; ``version`` is automatically set to the libtorrent version you're using @@ -4790,6 +4792,10 @@ expects to receive a lot of connections, or used in a simulator or test, it might make sense to raise this number. It will not take affect until listen_on() is called again (or for the first time). +if ``announce_double_nat`` is true, the ``&ip=`` argument in tracker requests +(unless otherwise specified) will be set to the intermediate IP address, if the +user is double NATed. If ther user is not double NATed, this option has no affect. + pe_settings =========== diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index 542d50696..064b5b751 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -235,8 +235,8 @@ namespace libtorrent // called when a port mapping is successful, or a router returns // a failure to map a port - void on_port_mapping(int mapping, int port, error_code const& ec - , int nat_transport); + void on_port_mapping(int mapping, address const& ip, int port + , error_code const& ec, int nat_transport); bool is_aborted() const { return m_abort; } bool is_paused() const { return m_paused; } @@ -301,6 +301,7 @@ namespace libtorrent session_status status() const; void set_peer_id(peer_id const& id); void set_key(int key); + address listen_address() const; unsigned short listen_port() const; void abort(); @@ -581,6 +582,11 @@ namespace libtorrent struct listen_socket_t { listen_socket_t(): external_port(0) {} + + // this is typically empty but can be set + // to the WAN IP address of NAT-PMP or UPnP router + address external_address; + // this is typically set to the same as the local // listen port. In case a NAT port forward was // successfully opened, this will be set to the diff --git a/include/libtorrent/natpmp.hpp b/include/libtorrent/natpmp.hpp index c90f90b6f..8cb91fa23 100644 --- a/include/libtorrent/natpmp.hpp +++ b/include/libtorrent/natpmp.hpp @@ -42,7 +42,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/deadline_timer.hpp" #include -#include +#include namespace libtorrent { @@ -50,7 +50,7 @@ namespace libtorrent // int: port mapping index // int: external port // std::string: error message -typedef boost::function portmap_callback_t; +typedef boost::function portmap_callback_t; typedef boost::function log_callback_t; class TORRENT_EXPORT natpmp : public intrusive_ptr_base @@ -75,6 +75,7 @@ private: void update_mapping(int i, mutex::scoped_lock& l); void send_map_request(int i, mutex::scoped_lock& l); + void send_get_ip_address_request(mutex::scoped_lock& l); void resend_request(int i, error_code const& e); void on_reply(error_code const& e , std::size_t bytes_transferred); @@ -142,6 +143,9 @@ private: // used to receive responses in char m_response_buffer[16]; + // router external IP address + address m_external_ip; + // the endpoint we received the message from udp::endpoint m_remote; diff --git a/include/libtorrent/session_settings.hpp b/include/libtorrent/session_settings.hpp index 43a6335e9..6fae84183 100644 --- a/include/libtorrent/session_settings.hpp +++ b/include/libtorrent/session_settings.hpp @@ -251,6 +251,7 @@ namespace libtorrent , mixed_mode_algorithm(peer_proportional) , rate_limit_utp(false) , listen_queue_size(5) + , announce_double_nat(false) {} // libtorrent version. Used for forward binary compatibility @@ -992,6 +993,12 @@ namespace libtorrent // the number of connections to accept while we're // not waiting in an accept() call. int listen_queue_size; + + // if this is true, the &ip= argument in tracker requests + // (unless otherwise specified) will be set to the intermediate + // IP address if the user is double NATed. If ther user is not + // double NATed, this option does not have an affect + bool announce_double_nat; }; #ifndef TORRENT_DISABLE_DHT diff --git a/include/libtorrent/upnp.hpp b/include/libtorrent/upnp.hpp index 0d818a5e4..7d4de88e4 100644 --- a/include/libtorrent/upnp.hpp +++ b/include/libtorrent/upnp.hpp @@ -90,12 +90,13 @@ namespace libtorrent #endif // int: port-mapping index +// address: external address as queried from router // int: external port // std::string: error message // an empty string as error means success // a port-mapping index of -1 means it's // an informational log message -typedef boost::function portmap_callback_t; +typedef boost::function portmap_callback_t; typedef boost::function log_callback_t; class TORRENT_EXPORT upnp : public intrusive_ptr_base @@ -147,6 +148,9 @@ private: void on_upnp_xml(error_code const& e , libtorrent::http_parser const& p, rootdevice& d , http_connection& c); + void on_upnp_get_ip_address_response(error_code const& e + , libtorrent::http_parser const& p, rootdevice& d + , http_connection& c); void on_upnp_map_response(error_code const& e , libtorrent::http_parser const& p, rootdevice& d , int mapping, http_connection& c); @@ -159,6 +163,7 @@ private: void return_error(int mapping, int code, mutex::scoped_lock& l); void log(char const* msg, mutex::scoped_lock& l); + void get_ip_address(rootdevice& d); void delete_port_mapping(rootdevice& d, int i); void create_port_mapping(http_connection& c, rootdevice& d, int i); void post(upnp::rootdevice const& d, char const* soap @@ -247,6 +252,7 @@ private: std::string hostname; int port; std::string path; + address external_ip; int lease_duration; // true if the device supports specifying a diff --git a/src/http_tracker_connection.cpp b/src/http_tracker_connection.cpp index 6e4ce9cd8..eeb81965b 100644 --- a/src/http_tracker_connection.cpp +++ b/src/http_tracker_connection.cpp @@ -59,6 +59,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/io.hpp" #include "libtorrent/socket.hpp" #include "libtorrent/aux_/session_impl.hpp" +#include "libtorrent/broadcast_socket.hpp" // for is_local using namespace libtorrent; @@ -180,23 +181,34 @@ namespace libtorrent } else #endif - if (!settings.announce_ip.empty()) + if (!m_ses.settings().anonymous_mode) { - error_code ec; - if (!ec) url += "&ip=" + escape_string( - settings.announce_ip.c_str(), settings.announce_ip.size()); - } - - if (!tracker_req().ipv6.empty() && !i2p) - { - url += "&ipv6="; - url += tracker_req().ipv6; - } - - if (!tracker_req().ipv4.empty() && !i2p) - { - url += "&ipv4="; - url += tracker_req().ipv4; + if (!settings.announce_ip.empty()) + { + url += "&ip=" + escape_string( + settings.announce_ip.c_str(), settings.announce_ip.size()); + } + else if (m_ses.settings().announce_double_nat + && is_local(m_ses.listen_address())) + { + // only use the global external listen address here + // if it turned out to be on a local network + // since otherwise the tracker should use our + // source IP to determine our origin + url += "&ip=" + print_address(m_ses.listen_address()); + } + + if (!tracker_req().ipv6.empty() && !i2p) + { + url += "&ipv6="; + url += tracker_req().ipv6; + } + + if (!tracker_req().ipv4.empty() && !i2p) + { + url += "&ipv4="; + url += tracker_req().ipv4; + } } } diff --git a/src/natpmp.cpp b/src/natpmp.cpp index 9c1b45b0b..c013ab56c 100644 --- a/src/natpmp.cpp +++ b/src/natpmp.cpp @@ -120,6 +120,7 @@ void natpmp::rebind(address const& listen_interface) #endif m_socket.async_receive_from(asio::buffer(&m_response_buffer, 16) , m_remote, boost::bind(&natpmp::on_reply, self(), _1, _2)); + send_get_ip_address_request(l); for (std::vector::iterator i = m_mappings.begin() , end(m_mappings.end()); i != end; ++i) @@ -132,6 +133,20 @@ void natpmp::rebind(address const& listen_interface) } } +void natpmp::send_get_ip_address_request(mutex::scoped_lock& l) +{ + using namespace libtorrent::detail; + + char buf[2]; + char* out = buf; + write_uint8(0, out); // NAT-PMP version + write_uint8(0, out); // public IP address request opcode + log("==> get public IP address", l); + + error_code ec; + m_socket.send_to(asio::buffer(buf, sizeof(buf)), m_nat_endpoint, 0, ec); +} + bool natpmp::get_mapping(int index, int& local_port, int& external_port, int& protocol) const { mutex::scoped_lock l(m_mutex); @@ -164,7 +179,7 @@ void natpmp::disable(error_code const& ec, mutex::scoped_lock& l) i->protocol = none; int index = i - m_mappings.begin(); l.unlock(); - m_callback(index, 0, ec); + m_callback(index, address(), 0, ec); l.lock(); } close_impl(l); @@ -337,7 +352,7 @@ void natpmp::send_map_request(int i, mutex::scoped_lock& l) log(msg, l); error_code ec; - m_socket.send_to(asio::buffer(buf, 12), m_nat_endpoint, 0, ec); + m_socket.send_to(asio::buffer(buf, sizeof(buf)), m_nat_endpoint, 0, ec); m.map_sent = true; m.outstanding_request = true; if (m_abort) @@ -428,11 +443,45 @@ void natpmp::on_reply(error_code const& e error_code ec; m_send_timer.cancel(ec); + if (bytes_transferred < 12) + { + char msg[200]; + snprintf(msg, sizeof(msg), "received packet of invalid size: %d", bytes_transferred); + log(msg, l); + return; + } + char* in = m_response_buffer; int version = read_uint8(in); int cmd = read_uint8(in); int result = read_uint16(in); int time = read_uint32(in); + + // for some reason the Airport extreme responds with + // a cmd of 130 for the public IP request. However, the + // response is still identifiable by its size + // this might be a bug triggered by libtorrent not serializing + // its port mapping requests and the external IP request + if (cmd == 128 || bytes_transferred == 12) + { + // public IP request response + m_external_ip = address_v4(read_uint32(in)); + + char msg[200]; + snprintf(msg, sizeof(msg), "<== public IP address [ %s ]", print_address(m_external_ip).c_str()); + log(msg, l); + return; + + } + + if (bytes_transferred < 16) + { + char msg[200]; + snprintf(msg, sizeof(msg), "received packet of invalid size: %d", bytes_transferred); + log(msg, l); + return; + } + int private_port = read_uint16(in); int public_port = read_uint16(in); int lifetime = read_uint32(in); @@ -505,13 +554,14 @@ void natpmp::on_reply(error_code const& e m->expires = time_now() + hours(2); l.unlock(); - m_callback(index, 0, error_code(ev, get_libtorrent_category())); + m_callback(index, address(), 0, error_code(ev, get_libtorrent_category())); l.lock(); } else if (m->action == mapping_t::action_add) { l.unlock(); - m_callback(index, m->external_port, error_code(errors::no_error, get_libtorrent_category())); + m_callback(index, m_external_ip, m->external_port, + error_code(errors::no_error, get_libtorrent_category())); l.lock(); } diff --git a/src/session_impl.cpp b/src/session_impl.cpp index 7bc3e4613..23e923999 100644 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -3608,6 +3608,16 @@ namespace aux { return !m_listen_sockets.empty(); } + address session_impl::listen_address() const + { + for (std::list::const_iterator i = m_listen_sockets.begin() + , end(m_listen_sockets.end()); i != end; ++i) + { + if (i->external_address != address()) return i->external_address; + } + return address(); + } + unsigned short session_impl::listen_port() const { // if peer connections are set up to be received over a socks @@ -3667,7 +3677,7 @@ namespace aux { m_alerts.post_alert(portmap_log_alert(map_transport, msg)); } - void session_impl::on_port_mapping(int mapping, int port + void session_impl::on_port_mapping(int mapping, address const& ip, int port , error_code const& ec, int map_transport) { TORRENT_ASSERT(is_network_thread()); @@ -3685,8 +3695,12 @@ namespace aux { if (mapping == m_tcp_mapping[map_transport] && port != 0) { - if (!m_listen_sockets.empty()) + if (ip != address()) set_external_address(ip); + + if (!m_listen_sockets.empty()) { + m_listen_sockets.front().external_address = ip; m_listen_sockets.front().external_port = port; + } if (m_alerts.should_post()) m_alerts.post_alert(portmap_alert(mapping, port , map_transport)); @@ -4248,7 +4262,7 @@ namespace aux { natpmp* n = new (std::nothrow) natpmp(m_io_service , m_listen_interface.address() , boost::bind(&session_impl::on_port_mapping - , this, _1, _2, _3, 0) + , this, _1, _2, _3, _4, 0) , boost::bind(&session_impl::on_port_map_log , this, _1, 0)); if (n == 0) return 0; @@ -4280,7 +4294,7 @@ namespace aux { , m_listen_interface.address() , m_settings.user_agent , boost::bind(&session_impl::on_port_mapping - , this, _1, _2, _3, 1) + , this, _1, _2, _3, _4, 1) , boost::bind(&session_impl::on_port_map_log , this, _1, 1) , m_settings.upnp_ignore_nonrouters); diff --git a/src/upnp.cpp b/src/upnp.cpp index ab94e330d..8e1b36c01 100644 --- a/src/upnp.cpp +++ b/src/upnp.cpp @@ -960,7 +960,42 @@ void upnp::on_upnp_xml(error_code const& e return; } - if (num_mappings() > 0) update_map(d, 0, l); + d.upnp_connection.reset(new http_connection(m_io_service + , m_cc, boost::bind(&upnp::on_upnp_get_ip_address_response, self(), _1, _2 + , boost::ref(d), _5), true + , boost::bind(&upnp::get_ip_address, self(), boost::ref(d)))); + d.upnp_connection->start(d.hostname, to_string(d.port).elems + , seconds(10), 1); +} + +void upnp::get_ip_address(rootdevice& d) +{ + mutex::scoped_lock l(m_mutex); + + TORRENT_ASSERT(d.magic == 1337); + + if (!d.upnp_connection) + { + TORRENT_ASSERT(d.disabled); + char msg[200]; + snprintf(msg, sizeof(msg), "getting external IP address"); + log(msg, l); + return; + } + + char const* soap_action = "GetExternalIPAddress"; + + char soap[2048]; + error_code ec; + snprintf(soap, sizeof(soap), "\n" + "" + "" + "" + , soap_action, d.service_namespace + , soap_action); + + post(d, soap, soap_action, l); } void upnp::disable(error_code const& ec, mutex::scoped_lock& l) @@ -974,7 +1009,7 @@ void upnp::disable(error_code const& ec, mutex::scoped_lock& l) if (i->protocol == none) continue; i->protocol = none; l.unlock(); - m_callback(i - m_mappings.begin(), 0, ec); + m_callback(i - m_mappings.begin(), address(), 0, ec); l.lock(); } @@ -1012,6 +1047,29 @@ namespace } } + struct ip_address_parse_state: public error_code_parse_state + { + ip_address_parse_state(): in_ip_address(false) {} + bool in_ip_address; + std::string ip_address; + }; + + void find_ip_address(int type, char const* string, ip_address_parse_state& state) + { + find_error_code(type, string, state); + if (state.exit) return; + + if (type == xml_start_tag && !std::strcmp("NewExternalIPAddress", string)) + { + state.in_ip_address = true; + } + else if (type == xml_string && state.in_ip_address) + { + state.ip_address = string; + state.exit = true; + } + } + struct error_code_t { int code; @@ -1074,6 +1132,84 @@ namespace libtorrent #endif +void upnp::on_upnp_get_ip_address_response(error_code const& e + , libtorrent::http_parser const& p, rootdevice& d + , http_connection& c) +{ + boost::intrusive_ptr me(self()); + + mutex::scoped_lock l(m_mutex); + + TORRENT_ASSERT(d.magic == 1337); + if (d.upnp_connection && d.upnp_connection.get() == &c) + { + d.upnp_connection->close(); + d.upnp_connection.reset(); + } + + if (m_closing) return; + + if (e && e != asio::error::eof) + { + char msg[200]; + snprintf(msg, sizeof(msg), "error while getting external IP address: %s", e.message().c_str()); + log(msg, l); + if (num_mappings() > 0) update_map(d, 0, l); + return; + } + + if (!p.header_finished()) + { + log("error while getting external IP address: incomplete http message", l); + if (num_mappings() > 0) update_map(d, 0, l); + return; + } + + if (p.status_code() != 200) + { + char msg[200]; + snprintf(msg, sizeof(msg), "error while getting external IP address: %s", p.message().c_str()); + log(msg, l); + if (num_mappings() > 0) update_map(d, 0, l); + return; + } + + // response may look like + // + // + // + // 192.168.160.19 + // + // + // + + char msg[500]; + snprintf(msg, sizeof(msg), "get external IP address response: %s" + , std::string(p.get_body().begin, p.get_body().end).c_str()); + log(msg, l); + + ip_address_parse_state s; + xml_parse((char*)p.get_body().begin, (char*)p.get_body().end + , boost::bind(&find_ip_address, _1, _2, boost::ref(s))); + if (s.error_code != -1) + { + char msg[200]; + snprintf(msg, sizeof(msg), "error while getting external IP address, code: %u" + , s.error_code); + log(msg, l); + } + + if (!s.ip_address.empty()) { + snprintf(msg, sizeof(msg), "got router external IP address %s", s.ip_address.c_str()); + log(msg, l); + d.external_ip = address::from_string(s.ip_address.c_str(), ec); + } else { + log("failed to find external IP address in response", l); + } + + if (num_mappings() > 0) update_map(d, 0, l); +} + void upnp::on_upnp_map_response(error_code const& e , libtorrent::http_parser const& p, rootdevice& d, int mapping , http_connection& c) @@ -1188,7 +1324,7 @@ void upnp::on_upnp_map_response(error_code const& e if (s.error_code == -1) { l.unlock(); - m_callback(mapping, m.external_port, error_code()); + m_callback(mapping, d.external_ip, m.external_port, error_code()); l.lock(); if (d.lease_duration > 0) { @@ -1231,7 +1367,7 @@ void upnp::return_error(int mapping, int code, mutex::scoped_lock& l) error_string += e->msg; } l.unlock(); - m_callback(mapping, 0, error_code(code, upnp_category)); + m_callback(mapping, address(), 0, error_code(code, upnp_category)); l.lock(); } diff --git a/test/test_upnp.cpp b/test/test_upnp.cpp index 0414422e8..bf717228a 100644 --- a/test/test_upnp.cpp +++ b/test/test_upnp.cpp @@ -193,11 +193,11 @@ struct callback_info std::list callbacks; -void callback(int mapping, int port, error_code const& err) +void callback(int mapping, address const& ip, int port, error_code const& err) { callback_info info = {mapping, port, err}; callbacks.push_back(info); - std::cerr << "mapping: " << mapping << ", port: " << port + std::cerr << "mapping: " << mapping << ", port: " << port << ", IP: " << ip << ", error: \"" << err.message() << "\"\n"; //TODO: store the callbacks and verify that the ports were successful }