From 469414d486e6c672776e227d97928cfe31970e5e Mon Sep 17 00:00:00 2001 From: Arvid Norberg Date: Mon, 12 Sep 2011 03:51:49 +0000 Subject: [PATCH] initial BitTorrent over SSL support --- docs/manual.rst | 89 ++++ examples/client_test.cpp | 34 ++ include/libtorrent/alert_types.hpp | 15 + include/libtorrent/aux_/session_impl.hpp | 45 +- include/libtorrent/bt_peer_connection.hpp | 6 +- include/libtorrent/peer_connection.hpp | 6 +- include/libtorrent/torrent.hpp | 26 +- include/libtorrent/torrent_handle.hpp | 11 +- src/alert.cpp | 5 + src/bt_peer_connection.cpp | 34 +- src/http_connection.cpp | 4 + src/peer_connection.cpp | 53 ++- src/session_impl.cpp | 49 ++- src/torrent.cpp | 476 +++++++++++++++++----- src/torrent_handle.cpp | 17 +- 15 files changed, 688 insertions(+), 182 deletions(-) diff --git a/docs/manual.rst b/docs/manual.rst index 182a3c1f7..2a52851b2 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -2310,6 +2310,11 @@ Its declaration looks like this:: sha1_hash info_hash() const; + void set_ssl_certificate(std::string const& cert + , std::string const& private_key + , std::string const& dh_params + , std::string const& passphrase = ""); + bool operator==(torrent_handle const&) const; bool operator!=(torrent_handle const&) const; bool operator<(torrent_handle const&) const; @@ -3300,6 +3305,35 @@ somehow invalid or if the filenames are not allowed (and hence cannot be opened/ your filesystem. If such an error occurs, a file_error_alert_ is generated and all handles that refers to that torrent will become invalid. +set_ssl_certificate() +--------------------- + + :: + + void set_ssl_certificate(std::string const& cert, std::string const& private_key + , std::string const& dh_params, std::string const& passphrase = ""); + +For SSL torrents, use this to specify a path to a .pem file to use as this client's certificate. +The certificate must be signed by the certificate in the .torrent file to be valid. + +``cert`` is a path to the (signed) certificate in .pem format corresponding to this torrent. + +``private_key`` is a path to the private key for the specified certificate. This must be in .pem +format. + +``dh_params`` is a path to the Diffie-Hellman parameter file, which needs to be in .pem format. +You can generate this file using the openssl command like this: +``openssl dhparam -outform PEM -out dhparams.pem 512``. + +``passphrase`` may be specified if the private key is encrypted and requires a passphrase to +be decrypted. + +Note that when a torrent first starts up, and it needs a certificate, it will suspend connecting +to any peers until it has one. It's typically desirable to resume the torrent after setting the +ssl certificate. + +If you receive a torrent_need_cert_alert_, you need to call this to provide a valid cert. If you +don't have a cert you won't be allowed to connect to any peers. torrent_status ============== @@ -3423,6 +3457,8 @@ It contains the following fields:: bool ip_filter_applies; sha1_hash info_hash; + + int listen_port; }; ``handle`` is a handle to the torrent whose status the object represents. @@ -3702,6 +3738,11 @@ to this torrent. This defaults to true. ``info_hash`` is the info-hash of the torrent. +``listen_port`` is the listen port this torrent is listening on for new +connections, if the torrent has its own listen socket. Only SSL torrents +have their own listen sockets. If the torrent doesn't have one, and is +accepting connections on the single listen socket, this is 0. + peer_info ========= @@ -7128,6 +7169,21 @@ cache flush is complete and the torrent does no longer have any files open. // ... }; +torrent_need_cert_alert +----------------------- + +This is always posted for SSL torrents. This is a reminder to the client that +the torrent won't work unless torrent_handle::set_ssl_certificate() is called with +a valid certificate. Valid certificates MUST be signed by the SSL certificate +in the .torrent file. + +:: + + struct torrent_need_cert_alert: tracker_alert + { + // ... + }; + dht_announce_alert ------------------ @@ -8844,6 +8900,39 @@ This threshold is controlled by ``session_settings::whole_pieces_threshold``. *TODO: piece affinity by speed category* *TODO: piece priorities* +SSL torrents +============ + +Torrents may have an SSL root (CA) certificate embedded in them. Such torrents +are called *SSL torrents*. An SSL torrent talks to all bittorrent peers over SSL. +The protocols are layered like this:: + + +-----------------------+ + | BitTorrent protocol | + +-----------------------+ + | SSL | + +-----------+-----------+ + | TCP | uTP | + | +-----------+ + | | UDP | + +-----------+-----------+ + +During the SSL handshake, both peers need to authenticate by providing a certificate +that is signed by the private counterpart of the CA certificate found in the +.torrent file. These peer certificates are expected to be privided to peers through +some other means than bittorrent. Typically by a peer generating a certificate request +which is sent to the publisher of the torrent, and the publisher returning a signed +certificate. + +In libtorrent, `set_ssl_certificate()`_ in torrent_handle_ is used to tell libtorrent where +to find the peer certificate and the private key for it. When an SSL torrent is loaded, +the torrent_need_cert_alert_ is posted to remind the user to provide a certificate. + +In order for the client to know which torrent an incoming connection belongs to, in order +to provide the correct certificate, each SSL torrent opens their own dedicated listen socket. + +This feature is only available if libtorrent is build with openssl support (``TORRENT_USE_OPENSSL``). + filename checks =============== diff --git a/examples/client_test.cpp b/examples/client_test.cpp index 79fea34f8..fe580286a 100644 --- a/examples/client_test.cpp +++ b/examples/client_test.cpp @@ -850,6 +850,40 @@ void handle_alert(libtorrent::session& ses, libtorrent::alert* a { using namespace libtorrent; +#ifdef TORRENT_USE_OPENSSL + if (torrent_need_cert_alert* p = alert_cast(a)) + { + torrent_handle h = p->handle; + error_code ec; + file_status st; + std::string cert = combine_path("certificates", to_hex(h.info_hash().to_string())) + ".pem"; + std::string priv = combine_path("certificates", to_hex(h.info_hash().to_string())) + "_key.pem"; + stat_file(cert, &st, ec); + if (ec) + { + char msg[256]; + snprintf(msg, sizeof(msg), "ERROR. could not load certificate %s: %s\n", cert.c_str(), ec.message().c_str()); + if (g_log_file) fprintf(g_log_file, "[%s] %s\n", time_now_string(), msg); + return; + } + stat_file(priv, &st, ec); + if (ec) + { + char msg[256]; + snprintf(msg, sizeof(msg), "ERROR. could not load private key %s: %s\n", priv.c_str(), ec.message().c_str()); + if (g_log_file) fprintf(g_log_file, "[%s] %s\n", time_now_string(), msg); + return; + } + + char msg[256]; + snprintf(msg, sizeof(msg), "loaded certificate %s and key %s\n", cert.c_str(), priv.c_str()); + if (g_log_file) fprintf(g_log_file, "[%s] %s\n", time_now_string(), msg); + + h.set_ssl_certificate(cert, priv, "certificates/dhparams.pem", "test"); + h.resume(); + } +#endif + if (torrent_finished_alert* p = alert_cast(a)) { p->handle.set_max_connections(max_connections_per_torrent / 2); diff --git a/include/libtorrent/alert_types.hpp b/include/libtorrent/alert_types.hpp index 9291ecb01..5ee578fcc 100644 --- a/include/libtorrent/alert_types.hpp +++ b/include/libtorrent/alert_types.hpp @@ -1263,6 +1263,21 @@ namespace libtorrent error_code error; }; + struct TORRENT_EXPORT torrent_need_cert_alert: torrent_alert + { + torrent_need_cert_alert(torrent_handle const& h) + : torrent_alert(h) + {} + + TORRENT_DEFINE_ALERT(torrent_need_cert_alert); + + const static int static_category = alert::status_notification; + virtual std::string message() const; + virtual bool discardable() const { return false; } + + error_code error; + }; + } diff --git a/include/libtorrent/aux_/session_impl.hpp b/include/libtorrent/aux_/session_impl.hpp index 37f5b74af..8aa6ca9d1 100644 --- a/include/libtorrent/aux_/session_impl.hpp +++ b/include/libtorrent/aux_/session_impl.hpp @@ -119,6 +119,27 @@ namespace libtorrent struct bencode_map_entry; + 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 + // port that is open on the external (NAT) interface + // on the NAT box itself. This is the port that has + // to be published to peers, since this is the port + // the client is reachable through. + int external_port; + + // the actual socket + boost::shared_ptr sock; + }; + namespace aux { struct session_impl; @@ -349,7 +370,7 @@ namespace libtorrent torrent_handle find_torrent_handle(sha1_hash const& info_hash); - void announce_lsd(sha1_hash const& ih, bool broadcast = false); + void announce_lsd(sha1_hash const& ih, int port, bool broadcast = false); void save_state(entry* e, boost::uint32_t flags) const; void load_state(lazy_entry const* e); @@ -661,26 +682,6 @@ namespace libtorrent tcp::endpoint m_ipv6_interface; tcp::endpoint m_ipv4_interface; - 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 - // port that is open on the external (NAT) interface - // on the NAT box itself. This is the port that has - // to be published to peers, since this is the port - // the client is reachable through. - int external_port; - - // the actual socket - boost::shared_ptr sock; - }; // since we might be listening on multiple interfaces // we might need more than one listen socket std::list m_listen_sockets; @@ -697,7 +698,7 @@ namespace libtorrent boost::shared_ptr m_i2p_listen_socket; #endif - listen_socket_t setup_listener(tcp::endpoint ep, int retries + void setup_listener(listen_socket_t* s, tcp::endpoint ep, int retries , bool v6_only, int flags, error_code& ec); // the proxy used for bittorrent diff --git a/include/libtorrent/bt_peer_connection.hpp b/include/libtorrent/bt_peer_connection.hpp index c35b4240f..f9ba9b6ae 100644 --- a/include/libtorrent/bt_peer_connection.hpp +++ b/include/libtorrent/bt_peer_connection.hpp @@ -90,7 +90,8 @@ namespace libtorrent , boost::weak_ptr t , boost::shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo); + , policy::peer* peerinfo + , bool outgoing = true); // with this constructor we have been contacted and we still don't // know which torrent the connection belongs to @@ -98,7 +99,8 @@ namespace libtorrent aux::session_impl& ses , boost::shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo); + , policy::peer* peerinfo + , bool outgoing = false); void start(); diff --git a/include/libtorrent/peer_connection.hpp b/include/libtorrent/peer_connection.hpp index ee4201e47..481d9cff0 100644 --- a/include/libtorrent/peer_connection.hpp +++ b/include/libtorrent/peer_connection.hpp @@ -171,7 +171,8 @@ namespace libtorrent , boost::weak_ptr t , boost::shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo); + , policy::peer* peerinfo + , bool outgoing = true); // with this constructor we have been contacted and we still don't // know which torrent the connection belongs to @@ -179,7 +180,8 @@ namespace libtorrent aux::session_impl& ses , boost::shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo); + , policy::peer* peerinfo + , bool outgoing = false); // this function is called after it has been constructed and properly // reference counted. It is safe to call self() in this function diff --git a/include/libtorrent/torrent.hpp b/include/libtorrent/torrent.hpp index e193227f7..7d387ff5b 100644 --- a/include/libtorrent/torrent.hpp +++ b/include/libtorrent/torrent.hpp @@ -94,6 +94,7 @@ namespace libtorrent struct add_torrent_params; struct storage_interface; class bt_peer_connection; + struct listen_socket_t; namespace aux { @@ -839,7 +840,10 @@ namespace libtorrent void dequeue_torrent_check(); #ifdef TORRENT_USE_OPENSSL - void set_ssl_cert(std::string const& certificate, error_code& ec); + void set_ssl_cert(std::string const& certificate + , std::string const& private_key + , std::string const& dh_params + , std::string const& passphrase); bool is_ssl_torrent() const { return m_ssl_ctx; } boost::asio::ssl::context* ssl_ctx() const { return m_ssl_ctx.get(); } #endif @@ -928,7 +932,27 @@ namespace libtorrent piece_manager* m_storage; #ifdef TORRENT_USE_OPENSSL + // TODO: in order to save space, stick the ssl context + // and the listen_socket_t in a single struct and have + // a shared_pointer to that (or intrusive_ptr even) + boost::shared_ptr m_ssl_ctx; + + // listen socket used to accept incoming ssl connections + boost::shared_ptr m_ssl_acceptor; + + // SSL torrents have their own listen socket, so that + // we know which certificate to use for incoming connections + // these function are used for handling the torrent specific + // listen socket + void async_accept(boost::shared_ptr const& listener); + + void on_accept_ssl_connection(boost::shared_ptr const& s + , boost::weak_ptr listen_socket, error_code const& e); + + void ssl_handshake(error_code const& ec, boost::shared_ptr s); + + void init_ssl(std::string const& cert); #endif #ifdef TORRENT_DEBUG diff --git a/include/libtorrent/torrent_handle.hpp b/include/libtorrent/torrent_handle.hpp index f02446408..59920c720 100644 --- a/include/libtorrent/torrent_handle.hpp +++ b/include/libtorrent/torrent_handle.hpp @@ -252,8 +252,10 @@ namespace libtorrent bool resolve_countries() const; #endif - void set_ssl_certificates(std::string const& certificate - , error_code& ec); + void set_ssl_certificate(std::string const& certificate + , std::string const& private_key + , std::string const& dh_params + , std::string const& passphrase = ""); storage_interface* get_storage_impl() const; @@ -485,6 +487,7 @@ namespace libtorrent , need_save_resume(false) , ip_filter_applies(true) , info_hash(0) + , listen_port(0) {} // handle to the torrent @@ -701,6 +704,10 @@ namespace libtorrent // the info-hash for this torrent sha1_hash info_hash; + + // if this torrent has its own listen socket, this is + // the port it's listening on. Otherwise it's 0 + int listen_port; }; } diff --git a/src/alert.cpp b/src/alert.cpp index a5260b8dd..9cefb72e2 100644 --- a/src/alert.cpp +++ b/src/alert.cpp @@ -561,5 +561,10 @@ namespace libtorrent { return torrent_alert::message() + " removed"; } + std::string torrent_need_cert_alert::message() const + { + return torrent_alert::message() + " needs SSL certificate"; + } + } // namespace libtorrent diff --git a/src/bt_peer_connection.cpp b/src/bt_peer_connection.cpp index c22bf39d0..71ce5e754 100644 --- a/src/bt_peer_connection.cpp +++ b/src/bt_peer_connection.cpp @@ -99,9 +99,10 @@ namespace libtorrent , boost::weak_ptr tor , shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo) + , policy::peer* peerinfo + , bool outgoing) : peer_connection(ses, tor, s, remote - , peerinfo) + , peerinfo, outgoing) , m_state(read_protocol_identifier) #ifndef TORRENT_DISABLE_EXTENSIONS , m_upload_only_id(0) @@ -152,8 +153,9 @@ namespace libtorrent session_impl& ses , boost::shared_ptr s , tcp::endpoint const& remote - , policy::peer* peerinfo) - : peer_connection(ses, s, remote, peerinfo) + , policy::peer* peerinfo + , bool outgoing) + : peer_connection(ses, s, remote, peerinfo, outgoing) , m_state(read_protocol_identifier) #ifndef TORRENT_DISABLE_EXTENSIONS , m_upload_only_id(0) @@ -225,6 +227,10 @@ namespace libtorrent boost::shared_ptr t = associated_torrent().lock(); std::string const key = t->torrent_file().encryption_key(); if (key.size() == 32) out_enc_policy = pe_settings::disabled; + + // never try an encrypted connection when already using SSL + if (get_socket()->get >() || get_socket()->get >()) + out_enc_policy = pe_settings::disabled; #endif #ifdef TORRENT_VERBOSE_LOGGING char const* policy_name[] = {"forced", "enabled", "disabled"}; @@ -2937,6 +2943,18 @@ namespace libtorrent peer_log("*** unrecognized protocol header"); #endif +#ifdef TORRENT_USE_OPENSSL + if (get_socket()->get >() + || get_socket()->get >()) + { +#ifdef TORRENT_VERBOSE_LOGGING + peer_log("*** SSL peers are not allowed to use any other encryption"); +#endif + disconnect(errors::invalid_info_hash, 1); + return; + } +#endif // TORRENT_USE_OPENSSL + bool found_encrypted_torrent = false; #ifdef TORRENT_USE_OPENSSL if (!is_local()) @@ -3078,7 +3096,13 @@ namespace libtorrent std::copy(recv_buffer.begin + 8, recv_buffer.begin + 28 , (char*)info_hash.begin()); - attach_to_torrent(info_hash, m_encrypted && m_rc4_encrypted); +#ifndef TORRENT_DISABLE_ENCRYPTION + bool allow_encrypted = m_encrypted && m_rc4_encrypted; +#else + bool allow_encrypted = true; +#endif + + attach_to_torrent(info_hash, allow_encrypted); if (is_disconnecting()) return; } else diff --git a/src/http_connection.cpp b/src/http_connection.cpp index f7e811d09..1c2346ee2 100644 --- a/src/http_connection.cpp +++ b/src/http_connection.cpp @@ -87,6 +87,10 @@ http_connection::http_connection(io_service& ios, connection_queue& cc , m_abort(false) { TORRENT_ASSERT(!m_handler.empty()); + // TODO: if we were handed an SSL context, we should really + // verify the hostname of the web server as well. This is supported + // in boost starting with version 1.47.0. See ssl::rfc2818_verification + // and ssl::context::set_verify_callback } http_connection::~http_connection() diff --git a/src/peer_connection.cpp b/src/peer_connection.cpp index 1eee27831..4bd22b82c 100644 --- a/src/peer_connection.cpp +++ b/src/peer_connection.cpp @@ -79,7 +79,8 @@ namespace libtorrent , boost::weak_ptr tor , shared_ptr s , tcp::endpoint const& endp - , policy::peer* peerinfo) + , policy::peer* peerinfo + , bool outgoing) : #ifdef TORRENT_DEBUG m_last_choke(time_now() - hours(1)) @@ -139,7 +140,7 @@ namespace libtorrent , m_choke_rejects(0) , m_read_recurse(0) , m_fast_reconnect(false) - , m_active(true) + , m_active(outgoing) , m_peer_interested(false) , m_peer_choked(true) , m_interesting(false) @@ -149,8 +150,8 @@ namespace libtorrent , m_ignore_unchoke_slots(false) , m_have_all(false) , m_disconnecting(false) - , m_connecting(true) - , m_queued(true) + , m_connecting(outgoing) + , m_queued(outgoing) , m_request_large_blocks(false) , m_share_mode(false) , m_upload_only(false) @@ -203,9 +204,12 @@ namespace libtorrent error_code ec; m_logger = m_ses.create_log(m_remote.address().to_string(ec) + "_" + to_string(m_remote.port()).elems, m_ses.listen_port()); - peer_log(">>> OUTGOING_CONNECTION [ ep: %s transport: %s seed: %d p: %p]" + peer_log(">>> %s [ ep: %s transport: %s seed: %d p: %p ]" + , outgoing ? "OUTGOING_CONNECTION" : "INCOMING CONNECTION" , print_endpoint(m_remote).c_str() - , (m_socket->get()) ? "uTP connection" : "TCP connection" + , m_socket->get >() ? "SSL/TCP" + : m_socket->get >() ? "SSL/uTP" + : m_socket->get() ? "uTP" : "TCP" , m_peer_info ? m_peer_info->seed : 0, m_peer_info); #endif #ifdef TORRENT_DEBUG @@ -223,7 +227,8 @@ namespace libtorrent session_impl& ses , shared_ptr s , tcp::endpoint const& endp - , policy::peer* peerinfo) + , policy::peer* peerinfo + , bool outgoing) : #ifdef TORRENT_DEBUG m_last_choke(time_now() - hours(1)) @@ -282,7 +287,7 @@ namespace libtorrent , m_choke_rejects(0) , m_read_recurse(0) , m_fast_reconnect(false) - , m_active(false) + , m_active(outgoing) , m_peer_interested(false) , m_peer_choked(true) , m_interesting(false) @@ -292,8 +297,8 @@ namespace libtorrent , m_ignore_unchoke_slots(false) , m_have_all(false) , m_disconnecting(false) - , m_connecting(false) - , m_queued(false) + , m_connecting(outgoing) + , m_queued(outgoing) , m_request_large_blocks(false) , m_share_mode(false) , m_upload_only(false) @@ -347,7 +352,8 @@ namespace libtorrent TORRENT_ASSERT(m_socket->remote_endpoint(ec) == m_remote || ec); m_logger = m_ses.create_log(remote().address().to_string(ec) + "_" + to_string(remote().port()).elems, m_ses.listen_port()); - peer_log("<<< INCOMING_CONNECTION [ ep: %s transport: %s ]" + peer_log("<<< %s [ ep: %s transport: %s ]" + , outgoing ? "OUTGOING_CONNECTION" : "INCOMING CONNECTION" , print_endpoint(m_remote).c_str() , (m_socket->get()) ? "uTP connection" : "TCP connection"); #endif @@ -536,7 +542,7 @@ namespace libtorrent TORRENT_ASSERT(m_peer_info == 0 || m_peer_info->connection == this); boost::shared_ptr t = m_torrent.lock(); - if (!t) + if (!m_active) { tcp::socket::non_blocking_io ioc(true); error_code ec; @@ -556,7 +562,8 @@ namespace libtorrent if (m_remote.address().is_v4()) m_socket->set_option(type_of_service(m_ses.settings().peer_tos), ec); } - else if (t->ready_for_connections()) + + if (t && t->ready_for_connections()) { init(); } @@ -1132,6 +1139,16 @@ namespace libtorrent t.reset(); } +#ifdef TORRENT_USE_OPENSSL + if (t && t->is_ssl_torrent()) + { +#if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_ERROR_LOGGING + peer_log("*** can't attach to an ssl torrent"); +#endif + t.reset(); + } +#endif + if (!t) { // we couldn't find the torrent! @@ -3320,7 +3337,7 @@ namespace libtorrent peer_log("CONNECTION FAILED: %s", print_endpoint(m_remote).c_str()); #endif #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING - (*m_ses.m_logger) << "CONNECTION FAILED: " << print_endpoint(m_remote) << "\n"; + (*m_ses.m_logger) << time_now_string() << " CONNECTION FAILED: " << print_endpoint(m_remote) << "\n"; #endif #ifdef TORRENT_STATS @@ -5234,7 +5251,7 @@ namespace libtorrent return; } #if defined TORRENT_VERBOSE_LOGGING - peer_log(">>> ASYNC_CONENCT [ dst: %s ]", print_endpoint(m_remote).c_str()); + peer_log(">>> ASYNC_CONNECT [ dst: %s ]", print_endpoint(m_remote).c_str()); #endif #if defined TORRENT_ASIO_DEBUGGING add_outstanding_async("peer_connection::on_connection_complete"); @@ -5279,7 +5296,11 @@ namespace libtorrent if (m_disconnecting) return; m_last_receive = time_now(); - if (m_socket->get() && m_peer_info) + if ((m_socket->get() +#ifdef TORRENT_USE_OPENSSL + || m_socket->get >() +#endif + ) && m_peer_info) { m_peer_info->confirmed_supports_utp = true; m_peer_info->supports_utp = false; diff --git a/src/session_impl.cpp b/src/session_impl.cpp index a9254aa09..3679743fc 100644 --- a/src/session_impl.cpp +++ b/src/session_impl.cpp @@ -1771,41 +1771,40 @@ namespace aux { return m_ipv4_interface; } - session_impl::listen_socket_t session_impl::setup_listener(tcp::endpoint ep + void session_impl::setup_listener(listen_socket_t* s, tcp::endpoint ep , int retries, bool v6_only, int flags, error_code& ec) { - listen_socket_t s; - s.sock.reset(new socket_acceptor(m_io_service)); - s.sock->open(ep.protocol(), ec); + s->sock.reset(new socket_acceptor(m_io_service)); + s->sock->open(ep.protocol(), ec); if (ec) { #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING (*m_logger) << "failed to open socket: " << print_endpoint(ep) << ": " << ec.message() << "\n" << "\n"; #endif - return listen_socket_t(); + return; } if (flags & session::listen_reuse_address) { error_code err; // ignore errors here - s.sock->set_option(socket_acceptor::reuse_address(true), err); + s->sock->set_option(socket_acceptor::reuse_address(true), err); } #if TORRENT_USE_IPV6 if (ep.protocol() == tcp::v6()) { error_code err; // ignore errors here - s.sock->set_option(v6only(v6_only), err); + s->sock->set_option(v6only(v6_only), err); #ifdef TORRENT_WINDOWS #ifndef PROTECTION_LEVEL_UNRESTRICTED #define PROTECTION_LEVEL_UNRESTRICTED 10 #endif // enable Teredo on windows - s.sock->set_option(v6_protection_level(PROTECTION_LEVEL_UNRESTRICTED), err); + s->sock->set_option(v6_protection_level(PROTECTION_LEVEL_UNRESTRICTED), err); #endif } #endif - s.sock->bind(ep, ec); + s->sock->bind(ep, ec); while (ec && retries > 0) { #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING @@ -1818,7 +1817,7 @@ namespace aux { TORRENT_ASSERT_VAL(!ec, ec); --retries; ep.port(ep.port() + 1); - s.sock->bind(ep, ec); + s->sock->bind(ep, ec); } if (ec && !(flags & session::listen_no_system_port)) { @@ -1826,7 +1825,7 @@ namespace aux { // let the OS pick a port ep.port(0); ec = error_code(); - s.sock->bind(ep, ec); + s->sock->bind(ep, ec); } if (ec) { @@ -1839,10 +1838,10 @@ namespace aux { , print_endpoint(ep).c_str(), ec.message().c_str()); (*m_logger) << time_now_string() << msg << "\n"; #endif - return listen_socket_t(); + return; } - s.external_port = s.sock->local_endpoint(ec).port(); - if (!ec) s.sock->listen(m_settings.listen_queue_size, ec); + s->external_port = s->sock->local_endpoint(ec).port(); + if (!ec) s->sock->listen(m_settings.listen_queue_size, ec); if (ec) { if (m_alerts.should_post()) @@ -1853,22 +1852,21 @@ namespace aux { , print_endpoint(ep).c_str(), ec.message().c_str()); (*m_logger) << time_now_string() << msg << "\n"; #endif - return listen_socket_t(); + return; } // if we asked the system to listen on port 0, which // socket did it end up choosing? if (ep.port() == 0) - ep.port(s.sock->local_endpoint().port()); + ep.port(s->sock->local_endpoint().port()); if (m_alerts.should_post()) m_alerts.post_alert(listen_succeeded_alert(ep)); #if defined TORRENT_VERBOSE_LOGGING || defined TORRENT_LOGGING || defined TORRENT_ERROR_LOGGING (*m_logger) << time_now_string() << " listening on: " << ep - << " external port: " << s.external_port << "\n"; + << " external port: " << s->external_port << "\n"; #endif - return s; } void session_impl::open_listen_port(int flags, error_code& ec) @@ -1887,8 +1885,8 @@ namespace aux { // this means we should open two listen sockets // one for IPv4 and one for IPv6 - listen_socket_t s = setup_listener( - tcp::endpoint(address_v4::any(), m_listen_interface.port()) + listen_socket_t s; + setup_listener(&s, tcp::endpoint(address_v4::any(), m_listen_interface.port()) , m_listen_port_retries, false, flags, ec); if (s.sock) @@ -1906,8 +1904,7 @@ namespace aux { // only try to open the IPv6 port if IPv6 is installed if (supports_ipv6()) { - s = setup_listener( - tcp::endpoint(address_v6::any(), m_listen_interface.port()) + setup_listener(&s, tcp::endpoint(address_v6::any(), m_listen_interface.port()) , m_listen_port_retries, true, flags, ec); if (s.sock) @@ -1936,8 +1933,8 @@ namespace aux { // we should only open a single listen socket, that // binds to the given interface - listen_socket_t s = setup_listener( - m_listen_interface, m_listen_port_retries, false, flags, ec); + listen_socket_t s; + setup_listener(&s, m_listen_interface, m_listen_port_retries, false, flags, ec); if (s.sock) { @@ -4470,11 +4467,11 @@ namespace aux { return m_listen_sockets.front().external_port; } - void session_impl::announce_lsd(sha1_hash const& ih, bool broadcast) + void session_impl::announce_lsd(sha1_hash const& ih, int port, bool broadcast) { // use internal listen port for local peers if (m_lsd.get()) - m_lsd->announce(ih, m_listen_interface.port(), broadcast); + m_lsd->announce(ih, port, broadcast); } void session_impl::on_lsd_peer(tcp::endpoint peer, sha1_hash const& ih) diff --git a/src/torrent.cpp b/src/torrent.cpp index 342801014..89216e8a8 100644 --- a/src/torrent.cpp +++ b/src/torrent.cpp @@ -1264,15 +1264,290 @@ namespace libtorrent #endif -/* #ifdef TORRENT_USE_OPENSSL +/* bool verify_function(bool preverified, boost::asio::ssl::verify_context& ctx) { return false; } -#endif */ + void torrent::init_ssl(std::string const& cert) + { + using boost::asio::ssl::context; + + // this is needed for openssl < 1.0 to decrypt keys created by openssl 1.0+ + OpenSSL_add_all_algorithms(); + + // TODO: come up with something better + RAND_seed(&info_hash()[0], 20); + TORRENT_ASSERT(RAND_status() == 1); + + // create the SSL context for this torrent. We need to + // inject the root certificate, and no other, to + // verify other peers against + boost::shared_ptr ctx( + new (std::nothrow) context(m_ses.m_io_service, context::sslv23)); + + if (!ctx) + { + set_error(asio::error::no_memory, "SSL context"); + pause(); + return; + } + + ctx->set_options(context::default_workarounds + | boost::asio::ssl::context::no_sslv2 + | boost::asio::ssl::context::single_dh_use); + + error_code ec; + ctx->set_verify_mode(context::verify_peer + | context::verify_fail_if_no_peer_cert + | context::verify_client_once, ec); + if (ec) + { + set_error(ec, "SSL verify mode"); + pause(); + return; + } + + // this is used for debugging + /* +#error there's a bug where the async_handshake on the ssl_stream always succeeds, regardless of the certificate failing. It's not a trivial bug in asio, that's been tested with a small repro program. +ctx->set_verify_callback(verify_function, ec); + if (ec) + { + set_error(ec, "SSL verify callback"); + pause(); + return; + } + */ + SSL_CTX* ssl_ctx = ctx->impl(); + + // create a new x.509 certificate store + X509_STORE* cert_store = X509_STORE_new(); + if (!cert_store) + { + set_error(asio::error::no_memory, "x.509 certificate store"); + pause(); + return; + } + + // wrap the PEM certificate in a BIO, for openssl to read + BIO* bp = BIO_new_mem_buf((void*)cert.c_str(), cert.size()); + + // parse the certificate into OpenSSL's internal + // representation + X509* certificate = PEM_read_bio_X509_AUX(bp, 0, 0, 0); + + BIO_free(bp); + + if (!certificate) + { + X509_STORE_free(cert_store); + set_error(asio::error::no_memory, "x.509 certificate"); + pause(); + return; + } + + // add cert to cert_store + X509_STORE_add_cert(cert_store, certificate); + + // and lastly, replace the default cert store with ours + SSL_CTX_set_cert_store(ssl_ctx, cert_store); +#if 0 + char filename[100]; + snprintf(filename, sizeof(filename), "/tmp/%d.pem", rand()); + FILE* f = fopen(filename, "w+"); + fwrite(cert.c_str(), cert.size(), 1, f); + fclose(f); + ctx->load_verify_file(filename); +#endif + // if all went well, set the torrent ssl context to this one + m_ssl_ctx = ctx; + + // tell the client we need a cert for this torrent + alerts().post_alert(torrent_need_cert_alert(get_handle())); + + m_ssl_acceptor.reset(new listen_socket_t); + m_ses.setup_listener(m_ssl_acceptor.get() + , tcp::endpoint(address_v4::any(), m_ses.m_listen_interface.port()) + , m_ses.m_listen_port_retries + 10, false, 0, ec); + if (!m_ssl_acceptor->sock) + { + set_error(ec, "ssl listen port"); + pause(); + return; + } + + // TODO: issue UPnP and NAT-PMP for this socket + + async_accept(m_ssl_acceptor->sock); + + set_allow_peers(false); + } + + void torrent::async_accept(boost::shared_ptr const& listener) + { + boost::shared_ptr c(new socket_type(m_ses.m_io_service)); + c->instantiate >(m_ses.m_io_service, m_ssl_ctx.get()); + +#if defined TORRENT_ASIO_DEBUGGING + add_outstanding_async("torrent::on_accept_ssl_connection"); +#endif + listener->async_accept(c->get >()->next_layer() + , boost::bind(&torrent::on_accept_ssl_connection, shared_from_this(), c + , boost::weak_ptr(listener), _1)); + } + + void torrent::on_accept_ssl_connection(boost::shared_ptr const& s + , boost::weak_ptr listen_socket, error_code const& e) + { +#if defined TORRENT_ASIO_DEBUGGING + complete_async("torrent::on_accept_ssl_connection"); +#endif + // TODO: there's some code duplication with session_impl::on_accept_connection + + boost::shared_ptr listener = listen_socket.lock(); + if (!listener) return; + + if (e == asio::error::operation_aborted) return; + + if (m_abort) return; + + error_code ec; + if (e) + { + tcp::endpoint ep = listener->local_endpoint(ec); +#if defined(TORRENT_VERBOSE_LOGGING) || defined(TORRENT_LOGGING) + std::string msg = "error accepting connection on '" + + print_endpoint(ep) + "' " + e.message(); + (*m_ses.m_logger) << msg << "\n"; +#endif +#ifdef TORRENT_WINDOWS + // Windows sometimes generates this error. It seems to be + // non-fatal and we have to do another async_accept. + if (e.value() == ERROR_SEM_TIMEOUT) + { + async_accept(listener); + return; + } +#endif +#ifdef TORRENT_BSD + // Leopard sometimes generates an "invalid argument" error. It seems to be + // non-fatal and we have to do another async_accept. + if (e.value() == EINVAL) + { + async_accept(listener); + return; + } +#endif + if (alerts().should_post()) + alerts().post_alert(listen_failed_alert(ep, e)); + return; + } + async_accept(listener); + + if (is_paused()) return; + + // we got a connection request! + tcp::endpoint endp = s->remote_endpoint(ec); + + if (ec) + { +#if defined(TORRENT_VERBOSE_LOGGING) || defined(TORRENT_LOGGING) + (*m_ses.m_logger) << endp << " <== INCOMING CONNECTION FAILED, could " + "not retrieve remote endpoint " << ec.message() << "\n"; +#endif + return; + } + + if (!settings().enable_incoming_tcp) + { + if (alerts().should_post()) + alerts().post_alert(peer_blocked_alert(torrent_handle(), endp.address())); + return; + } + + if (m_apply_ip_filter + && m_ses.m_ip_filter.access(endp.address()) & ip_filter::blocked) + { +#if defined(TORRENT_VERBOSE_LOGGING) || defined(TORRENT_LOGGING) + (*m_ses.m_logger) << "filtered blocked ip\n"; +#endif + if (alerts().should_post()) + alerts().post_alert(peer_blocked_alert(get_handle(), endp.address())); + return; + } + + if (m_connections.size() >= m_max_connections) + { + if (alerts().should_post()) + { + alerts().post_alert( + peer_disconnected_alert(get_handle(), endp, peer_id() + , error_code(errors::too_many_connections, get_libtorrent_category()))); + } + return; + } + + m_ses.setup_socket_buffers(*s); + + s->get >()->async_accept_handshake(boost::bind(&torrent::ssl_handshake + , shared_from_this(), _1, s)); + } + + void torrent::ssl_handshake(error_code const& ec, boost::shared_ptr s) + { + error_code e; + tcp::endpoint endp = s->remote_endpoint(e); + if (e) return; + +#if defined(TORRENT_VERBOSE_LOGGING) || defined(TORRENT_LOGGING) + (*m_ses.m_logger) << time_now_string() << " *** peer SSL handshake done [ ip: " + << endp << " ec: " << ec.message() << "]\n"; +#endif + + if (ec) + { + if (alerts().should_post()) + { + alerts().post_alert(peer_error_alert(get_handle(), endp + , peer_id(), ec)); + } + return; + } + + boost::intrusive_ptr c( + new bt_peer_connection(m_ses, shared_from_this(), s, endp, 0, false)); +#if defined TORRENT_DEBUG || TORRENT_RELEASE_ASSERTS + c->m_in_constructor = false; +#endif + + if (c->is_disconnecting()) return; + + if (!m_policy.new_connection(*c, m_ses.session_time())) + { +#if defined TORRENT_LOGGING + (*m_ses.m_logger) << time_now_string() << " CLOSING CONNECTION " + << p->remote() << " policy::new_connection returned false (i.e. peer list full)\n"; +#endif + c->disconnect(errors::too_many_connections); + return; + } + + // add the newly connected peer to this torrent's peer list + m_connections.insert(boost::get_pointer(c)); + m_ses.m_connections.insert(c); + c->start(); + if (settings().default_peer_upload_rate) + c->set_upload_limit(settings().default_peer_upload_rate); + if (settings().default_peer_download_rate) + c->set_download_limit(settings().default_peer_download_rate); + } + +#endif + // this may not be called from a constructor because of the call to // shared_from_this() void torrent::init() @@ -1284,88 +1559,7 @@ namespace libtorrent #ifdef TORRENT_USE_OPENSSL std::string cert = m_torrent_file->ssl_cert(); - if (!cert.empty()) - { - using boost::asio::ssl::context; - - // create the SSL context for this torrent. We need to - // inject the root certificate, and no other, to - // verify other peers against - boost::shared_ptr ctx( - new (std::nothrow) context(m_ses.m_io_service, context::sslv23)); - - if (!ctx) - { - set_error(asio::error::no_memory, "SSL context"); - pause(); - return; - } - - error_code ec; - ctx->set_verify_mode(context::verify_peer - | context::verify_fail_if_no_peer_cert, ec); - if (ec) - { - set_error(ec, "SSL context"); - pause(); - return; - } - -// this is used for debugging -/* -#error there's a bug where the async_handshake on the ssl_stream always succeeds, regardless of the certificate failing. It's not a trivial bug in asio, that's been tested with a small repro program. - ctx->set_verify_callback(verify_function, ec); - if (ec) - { - set_error(ec, "SSL verify callback"); - pause(); - return; - } -*/ - SSL_CTX* ssl_ctx = ctx->impl(); - - // create a new x.509 certificate store - X509_STORE* cert_store = X509_STORE_new(); - if (!cert_store) - { - set_error(asio::error::no_memory, "x.509 certificate store"); - pause(); - return; - } - - // wrap the PEM certificate in a BIO, for openssl to read - BIO* bp = BIO_new_mem_buf((void*)cert.c_str(), cert.size()); - - // parse the certificate into OpenSSL's internal - // representation - X509* certificate = PEM_read_bio_X509_AUX(bp, 0, 0, 0); - - BIO_free(bp); - - if (!certificate) - { - X509_STORE_free(cert_store); - set_error(asio::error::no_memory, "x.509 certificate"); - pause(); - return; - } - - // add cert to cert_store - X509_STORE_add_cert(cert_store, certificate); - - // and lastly, replace the default cert store with ours - SSL_CTX_set_cert_store(ssl_ctx, cert_store); -#if 0 - char filename[100]; - snprintf(filename, sizeof(filename), "/tmp/%d.pem", rand()); - FILE* f = fopen(filename, "w+"); - fwrite(cert.c_str(), cert.size(), 1, f); - fclose(f); - ctx->load_verify_file(filename); -#endif - // if all went well, set the torrent ssl context to this one - m_ssl_ctx = ctx; - } + if (!cert.empty()) init_ssl(cert); #endif #ifdef TORRENT_USE_OPENSSL @@ -1968,8 +2162,14 @@ namespace libtorrent if (is_paused()) return; +#ifdef TORRENT_USE_OPENSSL + int port = is_ssl_torrent() ? m_ssl_acceptor->external_port : m_ses.listen_port(); +#else + int port = m_ses.listen_port(); +#endif + // announce with the local discovery service - m_ses.announce_lsd(m_torrent_file->info_hash() + m_ses.announce_lsd(m_torrent_file->info_hash(), port , m_ses.settings().broadcast_lsd && m_lsd_seq == 0); ++m_lsd_seq; } @@ -1984,9 +2184,15 @@ namespace libtorrent TORRENT_ASSERT(m_allow_peers); +#ifdef TORRENT_USE_OPENSSL + int port = is_ssl_torrent() ? m_ssl_acceptor->external_port : m_ses.listen_port(); +#else + int port = m_ses.listen_port(); +#endif + boost::weak_ptr self(shared_from_this()); m_ses.m_dht->announce(m_torrent_file->info_hash() - , m_ses.listen_port(), is_seed() + , port, is_seed() , boost::bind(&torrent::on_dht_announce_response_disp, self, _1)); } @@ -2073,6 +2279,14 @@ namespace libtorrent req.num_want = (req.event == tracker_request::stopped) ?0:settings().num_want; + // SSL torrents use their own listen socket +#ifdef TORRENT_USE_OPENSSL + // TODO: this pattern is repeated in a few places. Factor this into + // a function and generalize the concept of a torrent having a + // dedicated listen port + if (is_ssl_torrent()) req.listen_port = m_ssl_acceptor->external_port; + else +#endif req.listen_port = m_ses.listen_port(); req.key = m_ses.m_key; @@ -3207,6 +3421,12 @@ namespace libtorrent m_ses.m_encrypted_torrents.erase(shared_from_this()); m_in_encrypted_list = false; } + + if (m_ssl_acceptor && m_ssl_acceptor->sock) + { + error_code ec; + m_ssl_acceptor->sock->close(ec); + } #endif m_abort = true; @@ -3973,22 +4193,51 @@ namespace libtorrent } #ifdef TORRENT_USE_OPENSSL - // certificate is a filename to a .pem file which is our - // certificate. root_cert is a filename to a certificate - // on disk which is the trusted certificate authority (CA) - // for this torrent. 'certificate' must be signed by the - // 'root_cert', and any peer we connect to or that connect - // to use must present a valid certificate signed by 'root_cert' - void torrent::set_ssl_cert(std::string const& certificate, error_code& ec) + std::string password_callback(int length, boost::asio::ssl::context::password_purpose p + , std::string pw) { - ec.clear(); - if (!m_ssl_ctx) - { - ec = asio::error::operation_not_supported; - return; - } + if (p != boost::asio::ssl::context::for_reading) return ""; + return pw; + } + + // certificate is a filename to a .pem file which is our + // certificate. The certificate must be signed by the root + // cert of the torrent file. any peer we connect to or that + // connect to use must present a valid certificate signed + // by the torrent root cert as well + void torrent::set_ssl_cert(std::string const& certificate + , std::string const& private_key + , std::string const& dh_params + , std::string const& passphrase) + { + if (!m_ssl_ctx) return; + using boost::asio::ssl::context; + error_code ec; + m_ssl_ctx->set_password_callback(boost::bind(&password_callback, _1, _2, passphrase), ec); + if (ec) + { + if (alerts().should_post()) + alerts().post_alert(torrent_error_alert(get_handle(), ec)); + } m_ssl_ctx->use_certificate_file(certificate, context::pem, ec); + if (ec) + { + if (alerts().should_post()) + alerts().post_alert(torrent_error_alert(get_handle(), ec)); + } + m_ssl_ctx->use_private_key_file(private_key, context::pem, ec); + if (ec) + { + if (alerts().should_post()) + alerts().post_alert(torrent_error_alert(get_handle(), ec)); + } + m_ssl_ctx->use_tmp_dh_file(dh_params, ec); + if (ec) + { + if (alerts().should_post()) + alerts().post_alert(torrent_error_alert(get_handle(), ec)); + } } #endif @@ -4022,7 +4271,7 @@ namespace libtorrent if (m_picker.get()) { bitfield const& pieces = p->get_bitfield(); - TORRENT_ASSERT(pieces.count() < int(pieces.size())); + TORRENT_ASSERT(pieces.count() <= int(pieces.size())); m_picker->dec_refcount(pieces); } } @@ -5159,6 +5408,9 @@ namespace libtorrent } #endif + // extend connect timeout by this many seconds + int timeout_extend = 0; + TORRENT_ASSERT(want_more_peers() || ignore_limit); TORRENT_ASSERT(m_ses.num_connections() < m_ses.settings().connections_limit || ignore_limit); @@ -5178,6 +5430,8 @@ namespace libtorrent s->get()->set_destination(static_cast(peerinfo)->destination); s->get()->set_command(i2p_stream::cmd_connect); s->get()->set_session_id(m_ses.m_i2p_conn.session_id()); + // i2p setups are slow + timeout_extend = 20; } else #endif @@ -5196,7 +5450,20 @@ namespace libtorrent // don't make a TCP connection if it's disabled if (sm == 0 && !m_ses.m_settings.enable_outgoing_tcp) return false; - bool ret = instantiate_connection(m_ses.m_io_service, m_ses.proxy(), *s, 0, sm, true); + void* userdata = 0; +#ifdef TORRENT_USE_OPENSSL + if (is_ssl_torrent()) + { + userdata = m_ssl_ctx.get(); + // SSL handshakes are slow + timeout_extend = 10; + + // we don't support SSL over uTP yet + sm = 0; + } +#endif + + bool ret = instantiate_connection(m_ses.m_io_service, m_ses.proxy(), *s, userdata, sm, true); (void)ret; TORRENT_ASSERT(ret); } @@ -5239,6 +5506,7 @@ namespace libtorrent int timeout = settings().peer_connect_timeout; if (peerinfo) timeout += 3 * peerinfo->failcount; + timeout += timeout_extend; TORRENT_TRY { @@ -5379,6 +5647,7 @@ namespace libtorrent (*m_ses.m_logger) << time_now_string() << " CLOSING CONNECTION " << p->remote() << " policy::new_connection returned false (i.e. peer list full)\n"; #endif + p->disconnect(errors::too_many_connections); return false; } } @@ -7675,6 +7944,11 @@ namespace libtorrent st->handle = get_handle(); st->info_hash = info_hash(); + st->listen_port = 0; +#ifdef TORRENT_USE_OPENSSL + if (is_ssl_torrent() && m_ssl_acceptor) st->listen_port = m_ssl_acceptor->external_port; +#endif + st->has_incoming = m_has_incoming; if (m_error) st->error = m_error.message() + ": " + m_error_file; st->seed_mode = m_seed_mode; diff --git a/src/torrent_handle.cpp b/src/torrent_handle.cpp index 6d7ee9201..6eeb659ae 100644 --- a/src/torrent_handle.cpp +++ b/src/torrent_handle.cpp @@ -112,6 +112,12 @@ namespace libtorrent session_impl& ses = t->session(); \ ses.m_io_service.post(boost::bind(&torrent:: x, t, a1, a2, a3)) +#define TORRENT_ASYNC_CALL4(x, a1, a2, a3, a4) \ + boost::shared_ptr t = m_torrent.lock(); \ + if (!t) return; \ + session_impl& ses = t->session(); \ + ses.m_io_service.post(boost::bind(&torrent:: x, t, a1, a2, a3, a4)) + #define TORRENT_SYNC_CALL(x) \ boost::shared_ptr t = m_torrent.lock(); \ if (!t) return; \ @@ -375,13 +381,14 @@ namespace libtorrent TORRENT_ASYNC_CALL(flush_cache); } - void torrent_handle::set_ssl_certificates( - std::string const& certificate, error_code& ec) + void torrent_handle::set_ssl_certificate( + std::string const& certificate + , std::string const& private_key + , std::string const& dh_params + , std::string const& passphrase) { #ifdef TORRENT_USE_OPENSSL - TORRENT_ASYNC_CALL2(set_ssl_cert, certificate, boost::ref(ec)); -#else - ec = boost::asio::error::operation_not_supported; + TORRENT_ASYNC_CALL4(set_ssl_cert, certificate, private_key, dh_params, passphrase); #endif }