diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a5964c25..9c5559467 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ set(sources i2p_stream identify_client ip_filter + ip_voter peer_connection bt_peer_connection web_peer_connection diff --git a/Jamfile b/Jamfile index ea2bce8c9..1e101a2fa 100755 --- a/Jamfile +++ b/Jamfile @@ -490,6 +490,7 @@ SOURCES = http_parser identify_client ip_filter + ip_voter peer_connection bt_peer_connection web_connection_base diff --git a/include/libtorrent/Makefile.am b/include/libtorrent/Makefile.am index 2916acf77..efb9e604f 100644 --- a/include/libtorrent/Makefile.am +++ b/include/libtorrent/Makefile.am @@ -61,6 +61,7 @@ nobase_include_HEADERS = \ io_service.hpp \ io_service_fwd.hpp \ ip_filter.hpp \ + ip_voter.hpp \ lazy_entry.hpp \ lsd.hpp \ magnet_uri.hpp \ diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index 9f637522b..bb9439161 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -57,6 +57,7 @@ POSSIBILITY OF SUCH DAMAGE. #pragma warning(pop) #endif +#include "libtorrent/ip_voter.hpp" #include "libtorrent/torrent_handle.hpp" #include "libtorrent/entry.hpp" #include "libtorrent/socket.hpp" @@ -510,7 +511,8 @@ namespace libtorrent // implements dht_observer virtual void set_external_address(address const& ip , int source_type, address const& source); - address const& external_address() const { return m_external_address; } + + external_ip const& external_address() const; bool can_write_to_disk() const { return m_disk_thread.can_write(); } @@ -1151,36 +1153,9 @@ namespace libtorrent #ifdef TORRENT_UPNP_LOGGING std::ofstream m_upnp_log; #endif - // TODO: factor the IP voting out to its own type, maybe templated by the address type. Have one IPv4 vote and a separate IPv6 vote. Maybe even better, have one per local interface sockets can be bound to - struct external_ip_t - { - external_ip_t(): sources(0), num_votes(0) {} - bool add_vote(sha1_hash const& k, int type); - bool operator<(external_ip_t const& rhs) const - { - if (num_votes < rhs.num_votes) return true; - if (num_votes > rhs.num_votes) return false; - return sources < rhs.sources; - } - - // this is a bloom filter of the IPs that have - // reported this address - bloom_filter<16> voters; - // this is the actual external address - address addr; - // a bitmask of sources the reporters have come from - boost::uint16_t sources; - // the total number of votes for this IP - boost::uint16_t num_votes; - }; - - // this is a bloom filter of all the IPs that have - // been the first to report an external address. Each - // IP only gets to add a new item once. - bloom_filter<32> m_external_address_voters; - std::vector m_external_addresses; - address m_external_address; + // state for keeping track of external IPs + external_ip m_external_ip; #ifndef TORRENT_DISABLE_EXTENSIONS typedef std::list +#include "libtorrent/address.hpp" +#include "libtorrent/bloom_filter.hpp" + +namespace libtorrent +{ + // this is an object that keeps the state for a single external IP + // based on peoples votes + struct ip_voter + { + // returns true if a different IP is the top vote now + // i.e. we changed our idea of what our external IP is + bool cast_vote(address const& ip, int source_type, address const& sorce); + + address external_address() const { return m_external_address; } + + private: + + struct external_ip_t + { + external_ip_t(): sources(0), num_votes(0) {} + + bool add_vote(sha1_hash const& k, int type); + bool operator<(external_ip_t const& rhs) const + { + if (num_votes < rhs.num_votes) return true; + if (num_votes > rhs.num_votes) return false; + return sources < rhs.sources; + } + + // this is a bloom filter of the IPs that have + // reported this address + bloom_filter<16> voters; + // this is the actual external address + address addr; + // a bitmask of sources the reporters have come from + boost::uint16_t sources; + // the total number of votes for this IP + boost::uint16_t num_votes; + }; + + // this is a bloom filter of all the IPs that have + // been the first to report an external address. Each + // IP only gets to add a new item once. + bloom_filter<32> m_external_address_voters; + std::vector m_external_addresses; + address m_external_address; + }; + + // this keeps track of multiple external IPs (for now, just IPv6 and IPv4, but + // it could be extended to deal with loopback and local network addresses as well) + struct external_ip + { + // returns true if a different IP is the top vote now + // i.e. we changed our idea of what our external IP is + bool cast_vote(address const& ip, int source_type, address const& source); + + // the external IP as it would be observed from `ip` + address external_address(address const& ip) const; + + private: + + // for now, assume one external IPv4 and one external IPv6 address + // 0 = IPv4 1 = IPv6 + // TODO: instead, have one instance per possible subnet, global IPv4, global IPv6, loopback, 192.168.x.x, 10.x.x.x, etc. + ip_voter m_vote_group[2]; + }; + +} + +#endif + diff --git a/include/libtorrent/policy.hpp b/include/libtorrent/policy.hpp index 75ffc49e5..14a0b094c 100644 --- a/include/libtorrent/policy.hpp +++ b/include/libtorrent/policy.hpp @@ -50,6 +50,7 @@ namespace libtorrent class torrent; class peer_connection; + struct external_ip; // this is compressed as an unsigned floating point value // the top 13 bits are the mantissa and the low @@ -178,7 +179,7 @@ namespace libtorrent size_type total_download() const; size_type total_upload() const; - boost::uint32_t rank(tcp::endpoint const& external) const; + boost::uint32_t rank(external_ip const& external, int external_port) const; libtorrent::address address() const; char const* dest() const; @@ -442,7 +443,7 @@ namespace libtorrent bool compare_peer_erase(policy::peer const& lhs, policy::peer const& rhs) const; bool compare_peer(policy::peer const& lhs, policy::peer const& rhs - , tcp::endpoint const& external_ip) const; + , external_ip const& external, int source_port) const; iterator find_connect_candidate(int session_time); diff --git a/src/Makefile.am b/src/Makefile.am index f68ba2634..6d618c3be 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -56,6 +56,7 @@ libtorrent_rasterbar_la_SOURCES = \ identify_client.cpp \ instantiate_connection.cpp \ ip_filter.cpp \ + ip_voter.cpp \ lazy_bdecode.cpp \ logger.cpp \ lsd.cpp \ diff --git a/src/ip_voter.cpp b/src/ip_voter.cpp new file mode 100644 index 000000000..7f4c75f1f --- /dev/null +++ b/src/ip_voter.cpp @@ -0,0 +1,123 @@ +/* + +Copyright (c) 2013, Arvid Norberg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of the author 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 OWNER 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 "libtorrent/ip_voter.hpp" +#include "libtorrent/broadcast_socket.hpp" // for is_any() etc. +#include "libtorrent/socket_io.hpp" // for hash_address + +#include + +namespace libtorrent +{ + bool ip_voter::cast_vote(address const& ip + , int source_type, address const& source) + { + if (is_any(ip)) return false; + if (is_local(ip)) return false; + if (is_loopback(ip)) return false; + + // don't trust source that aren't connected to us + // on a different address family than the external + // IP they claim we have + if (ip.is_v4() != source.is_v4()) return false; + + // this is the key to use for the bloom filters + // it represents the identity of the voter + sha1_hash k; + hash_address(source, k); + + // do we already have an entry for this external IP? + std::vector::iterator i = std::find_if(m_external_addresses.begin() + , m_external_addresses.end(), boost::bind(&external_ip_t::addr, _1) == ip); + + if (i == m_external_addresses.end()) + { + // each IP only gets to add a new IP once + if (m_external_address_voters.find(k)) return false; + + if (m_external_addresses.size() > 40) + { + if (random() % 100 < 50) + return false; + + // use stable sort here to maintain the fifo-order + // of the entries with the same number of votes + // this will sort in ascending order, i.e. the lowest + // votes first. Also, the oldest are first, so this + // is a sort of weighted LRU. + std::stable_sort(m_external_addresses.begin(), m_external_addresses.end()); + + // erase the first element, since this is the + // oldest entry and the one with lowst number + // of votes. This makes sense because the oldest + // entry has had the longest time to receive more + // votes to be bumped up + m_external_addresses.erase(m_external_addresses.begin()); + } + m_external_addresses.push_back(external_ip_t()); + i = m_external_addresses.end() - 1; + i->addr = ip; + } + // add one more vote to this external IP + if (!i->add_vote(k, source_type)) return false; + + i = std::max_element(m_external_addresses.begin(), m_external_addresses.end()); + TORRENT_ASSERT(i != m_external_addresses.end()); + + if (i->addr == m_external_address) return false; + + m_external_address = i->addr; + m_external_address_voters.clear(); + + return true; + } + + bool ip_voter::external_ip_t::add_vote(sha1_hash const& k, int type) + { + sources |= type; + if (voters.find(k)) return false; + voters.set(k); + ++num_votes; + return true; + } + + bool external_ip::cast_vote(address const& ip, int source_type, address const& source) + { + return m_vote_group[ip.is_v6()].cast_vote(ip, source_type, source); + } + + address external_ip::external_address(address const& ip) const + { + return m_vote_group[ip.is_v6()].external_address(); + } +} + diff --git a/src/kademlia/dht_tracker.cpp b/src/kademlia/dht_tracker.cpp index 6a1e16af0..d1f4f9935 100644 --- a/src/kademlia/dht_tracker.cpp +++ b/src/kademlia/dht_tracker.cpp @@ -203,7 +203,7 @@ namespace libtorrent { namespace dht dht_tracker::dht_tracker(libtorrent::aux::session_impl& ses, rate_limited_udp_socket& sock , dht_settings const& settings, entry const* state) : m_dht(&ses, this, settings, extract_node_id(state) - , ses.external_address(), &ses) + , ses.external_address().external_address(address_v4()), &ses) , m_sock(sock) , m_last_new_key(time_now() - minutes(key_refresh)) , m_timer(sock.get_io_service()) diff --git a/src/peer_connection.cpp b/src/peer_connection.cpp index 4f47f2c52..d091b2e90 100644 --- a/src/peer_connection.cpp +++ b/src/peer_connection.cpp @@ -1114,7 +1114,8 @@ namespace libtorrent boost::uint32_t peer_connection::peer_rank() const { - return m_peer_info->rank(tcp::endpoint(m_ses.external_address(), m_ses.listen_port())); + return m_peer_info->rank(m_ses.external_address() + , m_ses.listen_port()); } // message handlers diff --git a/src/policy.cpp b/src/policy.cpp index cbeb17b69..0f6f53b9b 100644 --- a/src/policy.cpp +++ b/src/policy.cpp @@ -742,7 +742,8 @@ namespace libtorrent TORRENT_ASSERT(m_finished == m_torrent->is_finished()); int min_reconnect_time = m_torrent->settings().min_reconnect_time; - tcp::endpoint external_ip(m_torrent->session().external_address(), m_torrent->session().listen_port()); + external_ip const& external = m_torrent->session().external_address(); + int external_port = m_torrent->session().listen_port(); if (m_round_robin >= int(m_peers.size())) m_round_robin = 0; @@ -809,7 +810,7 @@ namespace libtorrent // pe, which is the peer m_round_robin points to. If it is, just // keep looking. if (candidate != -1 - && compare_peer(*m_peers[candidate], pe, external_ip)) continue; + && compare_peer(*m_peers[candidate], pe, external, external_port)) continue; if (pe.last_connected && session_time - pe.last_connected < @@ -831,9 +832,9 @@ namespace libtorrent (*m_torrent->session().m_logger) << time_now_string() << " *** FOUND CONNECTION CANDIDATE [" " ip: " << m_peers[candidate]->ip() << - " d: " << cidr_distance(external_ip.address(), m_peers[candidate]->address()) << - " rank: " << m_peers[candidate]->rank(external_ip) << - " external: " << external_ip.address() << + " d: " << cidr_distance(external.address(m_peers[candidate]->address()), m_peers[candidate]->address()) << + " rank: " << m_peers[candidate]->rank(external, external_port) << + " external: " << external.external_address(m_peers[candidate]->address()) << " t: " << (session_time - m_peers[candidate]->last_connected) << " ]\n"; } @@ -1903,12 +1904,13 @@ namespace libtorrent } // TOOD: pass in both an IPv6 and IPv4 address here - boost::uint32_t policy::peer::rank(tcp::endpoint const& external) const + boost::uint32_t policy::peer::rank(external_ip const& external, int external_port) const { -//TODO: really, keep track of one external IP per address family -//TODO: how do we deal with our external address changing? +//TODO: how do we deal with our external address changing? Pass in a force-update maybe? and keep a version number in policy if (peer_rank == 0) - peer_rank = peer_priority(external, tcp::endpoint(this->address(), this->port)); + peer_rank = peer_priority( + tcp::endpoint(external.external_address(this->address()), external_port) + , tcp::endpoint(this->address(), this->port)); return peer_rank; } @@ -1963,7 +1965,7 @@ namespace libtorrent // this returns true if lhs is a better connect candidate than rhs bool policy::compare_peer(policy::peer const& lhs, policy::peer const& rhs - , tcp::endpoint const& external_ip) const + , external_ip const& external, int external_port) const { // prefer peers with lower failcount if (lhs.failcount != rhs.failcount) @@ -1990,8 +1992,8 @@ namespace libtorrent if (lhs_as != rhs_as) return lhs_as > rhs_as; } #endif - boost::uint32_t lhs_peer_rank = lhs.rank(external_ip); - boost::uint32_t rhs_peer_rank = rhs.rank(external_ip); + boost::uint32_t lhs_peer_rank = lhs.rank(external, external_port); + boost::uint32_t rhs_peer_rank = rhs.rank(external, external_port); if (lhs_peer_rank > rhs_peer_rank) return true; return false; } diff --git a/src/session_impl.cpp b/src/session_impl.cpp index 238e910c1..30ee92499 100644 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -6084,93 +6084,23 @@ retry: } m_upnp = 0; } - - bool session_impl::external_ip_t::add_vote(sha1_hash const& k, int type) - { - sources |= type; - if (voters.find(k)) return false; - voters.set(k); - ++num_votes; - return true; - } + + external_ip const& session_impl::external_address() const + { return m_external_ip; } void session_impl::set_external_address(address const& ip , int source_type, address const& source) { - if (is_any(ip)) return; - if (is_local(ip)) return; - if (is_loopback(ip)) return; - #if defined TORRENT_VERBOSE_LOGGING session_log(": set_external_address(%s, %d, %s)", print_address(ip).c_str() , source_type, print_address(source).c_str()); #endif - // this is the key to use for the bloom filters - // it represents the identity of the voter - sha1_hash k; - hash_address(source, k); - // do we already have an entry for this external IP? - std::vector::iterator i = std::find_if(m_external_addresses.begin() - , m_external_addresses.end(), boost::bind(&external_ip_t::addr, _1) == ip); - - if (i == m_external_addresses.end()) - { - // each IP only gets to add a new IP once - if (m_external_address_voters.find(k)) return; - - if (m_external_addresses.size() > 20) - { - if (random() < UINT_MAX / 2) - { -#if defined TORRENT_VERBOSE_LOGGING - session_log(": More than 20 slots, dopped"); -#endif - return; - } - // use stable sort here to maintain the fifo-order - // of the entries with the same number of votes - // this will sort in ascending order, i.e. the lowest - // votes first. Also, the oldest are first, so this - // is a sort of weighted LRU. - std::stable_sort(m_external_addresses.begin(), m_external_addresses.end()); - // erase the first element, since this is the - // oldest entry and the one with lowst number - // of votes. This makes sense because the oldest - // entry has had the longest time to receive more - // votes to be bumped up -#if defined TORRENT_VERBOSE_LOGGING - session_log(" More than 20 slots, dopping %s (%d)" - , print_address(m_external_addresses.front().addr).c_str() - , m_external_addresses.front().num_votes); -#endif - m_external_addresses.erase(m_external_addresses.begin()); - } - m_external_addresses.push_back(external_ip_t()); - i = m_external_addresses.end() - 1; - i->addr = ip; - } - // add one more vote to this external IP - if (!i->add_vote(k, source_type)) return; - - i = std::max_element(m_external_addresses.begin(), m_external_addresses.end()); - TORRENT_ASSERT(i != m_external_addresses.end()); - -#if defined TORRENT_VERBOSE_LOGGING - for (std::vector::iterator j = m_external_addresses.begin() - , end(m_external_addresses.end()); j != end; ++j) - { - session_log("%s %s votes: %d", (j == i)?"-->":" " - , print_address(j->addr).c_str(), j->num_votes); - } -#endif - if (i->addr == m_external_address) return; + if (!m_external_ip.cast_vote(ip, source_type, source)); #if defined TORRENT_VERBOSE_LOGGING session_log(" external IP updated"); #endif - m_external_address = i->addr; - m_external_address_voters.clear(); if (m_alerts.should_post()) m_alerts.post_alert(external_ip_alert(ip)); @@ -6178,6 +6108,7 @@ retry: // since we have a new external IP now, we need to // restart the DHT with a new node ID #ifndef TORRENT_DISABLE_DHT + // TODO: we only need to do this if our global IPv4 address has changed if (m_dht) { entry s = m_dht->state(); diff --git a/test/test_primitives.cpp b/test/test_primitives.cpp index 9cbd47cba..b647dc9bd 100644 --- a/test/test_primitives.cpp +++ b/test/test_primitives.cpp @@ -51,6 +51,7 @@ POSSIBILITY OF SUCH DAMAGE. #include "libtorrent/bloom_filter.hpp" #include "libtorrent/aux_/session_impl.hpp" #include "libtorrent/rsa.hpp" +#include "libtorrent/ip_voter.hpp" #ifndef TORRENT_DISABLE_DHT #include "libtorrent/kademlia/node_id.hpp" #include "libtorrent/kademlia/routing_table.hpp" @@ -394,6 +395,13 @@ address rand_v4() return address_v4((rand() << 16 | rand()) & 0xffffffff); } +address rand_v6() +{ + address_v6::bytes_type bytes; + for (int i = 0; i < bytes.size(); ++i) bytes[i] = rand(); + return address_v6(bytes); +} + int test_main() { using namespace libtorrent; @@ -537,12 +545,7 @@ int test_main() } // test external ip voting - aux::session_impl* ses = new aux::session_impl(std::pair(0,0) - , fingerprint("LT", 0, 0, 0, 0), "0.0.0.0", 0 -#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING - , "" -#endif - ); + external_ip ipv1; // test a single malicious node // adds 50 legitimate responses from different peers @@ -551,39 +554,28 @@ int test_main() address malicious = address_v4::from_string("4.4.4.4"); for (int i = 0; i < 50; ++i) { - ses->set_external_address(real_external, aux::session_impl::source_dht, rand_v4()); - ses->set_external_address(rand_v4(), aux::session_impl::source_dht, malicious); + ipv1.cast_vote(real_external, aux::session_impl::source_dht, rand_v4()); + ipv1.cast_vote(rand_v4(), aux::session_impl::source_dht, malicious); } - TEST_CHECK(ses->external_address() == real_external); - ses->abort(); -#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING - ses->m_logger.reset(); -#endif - delete ses; - ses = new aux::session_impl(std::pair(0,0) - , fingerprint("LT", 0, 0, 0, 0), "0.0.0.0", 0 -#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING - , "" -#endif - ); + TEST_CHECK(ipv1.external_address(rand_v4()) == real_external); + + external_ip ipv2; // test a single malicious node // adds 50 legitimate responses from different peers // and 50 consistent malicious responses from the same peer - real_external = address_v4::from_string("5.5.5.5"); + address real_external1 = address_v4::from_string("5.5.5.5"); + address real_external2 = address_v6::from_string("2f80::1"); malicious = address_v4::from_string("4.4.4.4"); address malicious_external = address_v4::from_string("3.3.3.3"); for (int i = 0; i < 50; ++i) { - ses->set_external_address(real_external, aux::session_impl::source_dht, rand_v4()); - ses->set_external_address(malicious_external, aux::session_impl::source_dht, malicious); + ipv2.cast_vote(real_external1, aux::session_impl::source_dht, rand_v4()); + ipv2.cast_vote(real_external2, aux::session_impl::source_dht, rand_v6()); + ipv2.cast_vote(malicious_external, aux::session_impl::source_dht, malicious); } - TEST_CHECK(ses->external_address() == real_external); - ses->abort(); -#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING - ses->m_logger.reset(); -#endif - delete ses; + TEST_CHECK(ipv2.external_address(rand_v4()) == real_external1); + TEST_CHECK(ipv2.external_address(rand_v6()) == real_external2); // test bloom_filter bloom_filter<32> filter;