transmission/libtransmission/tr-lpd.cc

745 lines
21 KiB
C++

/*
Copyright (c) 2010 by Johannes Lieder
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#include <algorithm>
#include <cctype> /* toupper() */
#include <cerrno>
#include <csignal> /* sig_atomic_t */
#include <cstdio>
#include <cstring> /* strlen(), strncpy(), strstr(), memset() */
#ifdef _WIN32
#include <inttypes.h>
#include <ws2tcpip.h>
using in_port_t = uint16_t; /* all missing */
#else
#include <ctime>
#include <unistd.h> /* close() */
#include <sys/types.h>
#include <sys/socket.h> /* socket(), bind() */
#include <netinet/in.h> /* sockaddr_in */
#endif
/* third party */
#include <event2/event.h>
#include <event2/util.h>
/* libT */
#include "transmission.h"
#include "log.h"
#include "net.h"
#include "peer-mgr.h" /* tr_peerMgrAddPex() */
#include "session.h"
#include "torrent.h" /* tr_torrentFindFromHash() */
#include "tr-assert.h"
#include "tr-lpd.h"
#include "utils.h"
#include "version.h"
#define SIZEOF_HASH_STRING (sizeof(((struct tr_info*)0)->hashString))
/**
* @brief Local Peer Discovery
* @file tr-lpd.c
*
* This module implements the Local Peer Discovery (LPD) protocol as supported by the
* uTorrent client application. A typical LPD datagram is 119 bytes long.
*
*/
static void event_callback(evutil_socket_t, short, void*);
enum
{
UPKEEP_INTERVAL_SECS = 5
};
static struct event* upkeep_timer = nullptr;
static tr_socket_t lpd_socket; /**<separate multicast receive socket */
static tr_socket_t lpd_socket2; /**<and multicast send socket */
static struct event* lpd_event = nullptr;
static tr_port lpd_port;
static tr_session* session;
enum
{
lpd_maxDatagramLength = 200 /**<the size an LPD datagram must not exceed */
};
char const lpd_mcastGroup[] = "239.192.152.143"; /**<LPD multicast group */
int const lpd_mcastPort = 6771; /**<LPD source and destination UPD port */
static struct sockaddr_in lpd_mcastAddr; /**<initialized from the above constants in tr_lpdInit */
/**
* @brief Protocol-related information carried by a Local Peer Discovery packet */
struct lpd_protocolVersion
{
int major;
int minor;
};
enum lpd_enumTimeToLive
{
lpd_ttlSameSubnet = 1,
lpd_ttlSameSite = 32,
lpd_ttlSameRegion = 64,
lpd_ttlSameContinent = 128,
lpd_ttlUnrestricted = 255
};
enum
{
lpd_announceInterval = 4 * 60, /**<4 min announce interval per torrent */
lpd_announceScope = lpd_ttlSameSubnet /**<the maximum scope for LPD datagrams */
};
/**
* @defgroup DoS Message Flood Protection
* @{
* We want to have a means to protect the libtransmission backend against message
* flooding: the strategy is to cap event processing once more than ten messages
* per second (that is, taking the average over one of our housekeeping intervals)
* got into our processing handler.
* If we'd really hit the limit and start discarding events, we either joined an
* extremely crowded multicast group or a malevolent host is sending bogus data to
* our socket. In this situation, we rather miss some announcements than blocking
* the actual task.
* @}
*/
/**
* @ingroup DoS
* @brief allow at most ten messages per second (interval average)
* @note this constraint is only enforced once per housekeeping interval */
enum
{
lpd_announceCapFactor = 10
};
/**
* @ingroup DoS
* @brief number of unsolicited messages during the last HK interval
* @remark counts downwards */
static int lpd_unsolicitedMsgCounter;
/**
* @def CRLF
* @brief a line-feed, as understood by the LPD protocol */
#define CRLF "\r\n"
/**
* @defgroup HttpReqProc HTTP-style request handling
* @{
*/
/**
* @brief Checks for BT-SEARCH method and separates the parameter section
* @param[in] s The request string
* @param[out] ver If non-nullptr, gets filled with protocol info from the request
* @return Returns a relative pointer to the beginning of the parameter section.
* If result is nullptr, s was invalid and no information will be returned
* @remark Note that the returned pointer is only usable as long as the given
* pointer s is valid; that is, return storage is temporary.
*
* Determines whether the given string checks out to be a valid BT-SEARCH message.
* If so, the return value points to the beginning of the parameter section (note:
* in this case the function returns a character sequence beginning with CRLF).
* If parameter is not nullptr, the declared protocol version is returned as part
* of the lpd_protocolVersion structure.
*/
static char const* lpd_extractHeader(char const* s, struct lpd_protocolVersion* const ver)
{
TR_ASSERT(s != nullptr);
int major = -1;
int minor = -1;
size_t len = strlen(s);
/* something might be rotten with this chunk of data */
if (len == 0 || len > lpd_maxDatagramLength)
{
return nullptr;
}
/* now we can attempt to look up the BT-SEARCH header */
if (sscanf(s, "BT-SEARCH * HTTP/%d.%d" CRLF, &major, &minor) != 2)
{
return nullptr;
}
if (major < 0 || minor < 0)
{
return nullptr;
}
{
/* a pair of blank lines at the end of the string, no place else */
char const* const two_blank = CRLF CRLF CRLF;
char const* const end = strstr(s, two_blank);
if (end == nullptr || strlen(end) > strlen(two_blank))
{
return nullptr;
}
}
if (ver != nullptr)
{
ver->major = major;
ver->minor = minor;
}
/* separate the header, begins with CRLF */
return strstr(s, CRLF);
}
/**
* @brief Return the value of a named parameter
*
* @param[in] str Input string of "\r\nName: Value" pairs without HTTP-style method part
* @param[in] name Name of parameter to extract
* @param[in] n Maximum available storage for value to return
* @param[out] val Output parameter for the actual value
* @return Returns 1 if value could be copied successfully
*
* Extracts the associated value of a named parameter from a HTTP-style header by
* performing the following steps:
* - assemble search string "\r\nName: " and locate position
* - copy back value from end to next "\r\n"
*/
static bool lpd_extractParam(char const* const str, char const* const name, int n, char* const val)
{
TR_ASSERT(str != nullptr);
TR_ASSERT(name != nullptr);
TR_ASSERT(val != nullptr);
enum
{
/* configure maximum length of search string here */
maxLength = 30
};
char sstr[maxLength] = { 0 };
char const* pos;
if (strlen(name) > maxLength - strlen(CRLF ": "))
{
return false;
}
/* compose the string token to search for */
tr_snprintf(sstr, maxLength, CRLF "%s: ", name);
pos = strstr(str, sstr);
if (pos == nullptr)
{
return false; /* search was not successful */
}
{
char const* const beg = pos + strlen(sstr);
char const* const new_line = strstr(beg, CRLF);
/* the value is delimited by the next CRLF */
int const len = new_line - beg;
/* if value string hits the length limit n,
* leave space for a trailing '\0' character */
n = std::min(len, n - 1);
strncpy(val, beg, n);
val[n] = 0;
}
/* we successfully returned the value string */
return true;
}
/**
* @} */
static void on_upkeep_timer(evutil_socket_t, short, void*);
/**
* @brief Initializes Local Peer Discovery for this node
*
* For the most part, this means setting up an appropriately configured multicast socket
* and event-based message handling.
*
* @remark Since the LPD service does not use another protocol family yet, this code is
* IPv4 only for the time being.
*/
int tr_lpdInit(tr_session* ss, [[maybe_unused]] tr_address* tr_addr)
{
/* if this check fails (i.e. the definition of hashString changed), update
* string handling in tr_lpdSendAnnounce() and tr_lpdConsiderAnnounce().
* However, the code should work as long as interfaces to the rest of
* libtransmission are compatible with char* strings. */
TR_STATIC_ASSERT(sizeof(((struct tr_info*)0)->hashString[0]) == sizeof(char), "");
struct ip_mreq mcastReq;
int const opt_on = 1;
int const opt_off = 0;
if (session != nullptr) /* already initialized */
{
return -1;
}
TR_ASSERT(lpd_announceInterval > 0);
TR_ASSERT(lpd_announceScope > 0);
lpd_port = tr_sessionGetPeerPort(ss);
if (lpd_port <= 0)
{
return -1;
}
tr_logAddNamedDbg("LPD", "Initialising Local Peer Discovery");
/* setup datagram socket (receive) */
{
lpd_socket = socket(PF_INET, SOCK_DGRAM, 0);
if (lpd_socket == TR_BAD_SOCKET)
{
goto fail;
}
if (evutil_make_socket_nonblocking(lpd_socket) == -1)
{
goto fail;
}
if (setsockopt(lpd_socket, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<char const*>(&opt_on), sizeof(opt_on)) == -1)
{
goto fail;
}
memset(&lpd_mcastAddr, 0, sizeof(lpd_mcastAddr));
lpd_mcastAddr.sin_family = AF_INET;
lpd_mcastAddr.sin_port = htons(lpd_mcastPort);
if (evutil_inet_pton(lpd_mcastAddr.sin_family, lpd_mcastGroup, &lpd_mcastAddr.sin_addr) == -1)
{
goto fail;
}
if (bind(lpd_socket, (struct sockaddr*)&lpd_mcastAddr, sizeof(lpd_mcastAddr)) == -1)
{
goto fail;
}
/* we want to join that LPD multicast group */
memset(&mcastReq, 0, sizeof(mcastReq));
mcastReq.imr_multiaddr = lpd_mcastAddr.sin_addr;
mcastReq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(lpd_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast<char const*>(&mcastReq), sizeof(mcastReq)) ==
-1)
{
goto fail;
}
if (setsockopt(lpd_socket, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast<char const*>(&opt_off), sizeof(opt_off)) ==
-1)
{
goto fail;
}
}
/* setup datagram socket (send) */
{
unsigned char const scope = lpd_announceScope;
lpd_socket2 = socket(PF_INET, SOCK_DGRAM, 0);
if (lpd_socket2 == TR_BAD_SOCKET)
{
goto fail;
}
if (evutil_make_socket_nonblocking(lpd_socket2) == -1)
{
goto fail;
}
/* configure outbound multicast TTL */
if (setsockopt(lpd_socket2, IPPROTO_IP, IP_MULTICAST_TTL, reinterpret_cast<char const*>(&scope), sizeof(scope)) == -1)
{
goto fail;
}
if (setsockopt(lpd_socket2, IPPROTO_IP, IP_MULTICAST_LOOP, reinterpret_cast<char const*>(&opt_off), sizeof(opt_off)) ==
-1)
{
goto fail;
}
}
session = ss;
/* Note: lpd_unsolicitedMsgCounter remains 0 until the first timeout event, thus
* any announcement received during the initial interval will be discarded. */
lpd_event = event_new(ss->event_base, lpd_socket, EV_READ | EV_PERSIST, event_callback, nullptr);
event_add(lpd_event, nullptr);
upkeep_timer = evtimer_new(ss->event_base, on_upkeep_timer, ss);
tr_timerAdd(upkeep_timer, UPKEEP_INTERVAL_SECS, 0);
tr_logAddNamedDbg("LPD", "Local Peer Discovery initialised");
return 1;
fail:
{
int const save = errno;
evutil_closesocket(lpd_socket);
evutil_closesocket(lpd_socket2);
lpd_socket = lpd_socket2 = TR_BAD_SOCKET;
session = nullptr;
tr_logAddNamedDbg("LPD", "LPD initialisation failed (errno = %d)", save);
errno = save;
}
return -1;
}
void tr_lpdUninit(tr_session* ss)
{
if (session != ss)
{
return;
}
tr_logAddNamedDbg("LPD", "Uninitialising Local Peer Discovery");
event_free(lpd_event);
lpd_event = nullptr;
evtimer_del(upkeep_timer);
upkeep_timer = nullptr;
/* just shut down, we won't remember any former nodes */
evutil_closesocket(lpd_socket);
evutil_closesocket(lpd_socket2);
tr_logAddNamedDbg("LPD", "Done uninitialising Local Peer Discovery");
session = nullptr;
}
bool tr_lpdEnabled(tr_session const* ss)
{
return ss != nullptr && ss == session;
}
/**
* @endcond */
/**
* @defgroup LdsProto LPD announcement processing
* @{
*/
/**
* @brief Announce the given torrent on the local network
*
* @param[in] t Torrent to announce
* @return Returns true on success
*
* Send a query for torrent t out to the LPD multicast group (or the LAN, for that
* matter). A listening client on the same network might react by adding us to his
* peer pool for torrent t.
*/
bool tr_lpdSendAnnounce(tr_torrent const* t)
{
char const fmt[] = //
"BT-SEARCH * HTTP/%u.%u" CRLF //
"Host: %s:%u" CRLF //
"Port: %u" CRLF //
"Infohash: %s" CRLF //
"" CRLF //
"" CRLF;
char hashString[SIZEOF_HASH_STRING];
char query[lpd_maxDatagramLength + 1] = { 0 };
if (t == nullptr)
{
return false;
}
/* make sure the hash string is normalized, just in case */
for (size_t i = 0; i < TR_N_ELEMENTS(hashString); ++i)
{
hashString[i] = toupper(t->info.hashString[i]);
}
/* prepare a zero-terminated announce message */
tr_snprintf(query, lpd_maxDatagramLength + 1, fmt, 1, 1, lpd_mcastGroup, lpd_mcastPort, lpd_port, hashString);
/* actually send the query out using [lpd_socket2] */
{
int const len = strlen(query);
/* destination address info has already been set up in tr_lpdInit(),
* so we refrain from preparing another sockaddr_in here */
int res = sendto(lpd_socket2, query, len, 0, (struct sockaddr const*)&lpd_mcastAddr, sizeof(lpd_mcastAddr));
if (res != len)
{
return false;
}
}
tr_logAddTorDbg(t, "LPD announce message away");
return true;
}
/**
* @brief Process incoming unsolicited messages and add the peer to the announced
* torrent if all checks are passed.
*
* @param[in,out] peer Adress information of the peer to add
* @param[in] msg The announcement message to consider
* @return Returns 0 if any input parameter or the announce was invalid, 1 if the peer
* was successfully added, -1 if not; a non-null return value indicates a side-effect to
* the peer in/out parameter.
*
* @note The port information gets added to the peer structure if tr_lpdConsiderAnnounce
* is able to extract the necessary information from the announce message. That is, if
* return != 0, the caller may retrieve the value from the passed structure.
*/
static int tr_lpdConsiderAnnounce(tr_pex* peer, char const* const msg)
{
enum
{
maxValueLen = 25,
maxHashLen = SIZEOF_HASH_STRING
};
auto ver = lpd_protocolVersion{ -1, -1 };
char value[maxValueLen] = { 0 };
char hashString[maxHashLen] = { 0 };
int res = 0;
int peerPort = 0;
if (peer != nullptr && msg != nullptr)
{
tr_torrent* tor = nullptr;
char const* params = lpd_extractHeader(msg, &ver);
if (params == nullptr || ver.major != 1) /* allow messages of protocol v1 */
{
return 0;
}
/* save the effort to check Host, which seems to be optional anyway */
if (!lpd_extractParam(params, "Port", maxValueLen, value))
{
return 0;
}
/* determine announced peer port, refuse if value too large */
if (sscanf(value, "%d", &peerPort) != 1 || peerPort > (in_port_t)-1)
{
return 0;
}
peer->port = htons(peerPort);
res = -1; /* signal caller side-effect to peer->port via return != 0 */
if (!lpd_extractParam(params, "Infohash", maxHashLen, hashString))
{
return res;
}
tor = tr_torrentFindFromHashString(session, hashString);
if (tr_isTorrent(tor) && tr_torrentAllowsLPD(tor))
{
/* we found a suitable peer, add it to the torrent */
tr_peerMgrAddPex(tor, TR_PEER_FROM_LPD, peer, 1);
tr_logAddTorDbg(tor, "Learned %d local peer from LPD (%s:%u)", 1, tr_address_to_string(&peer->addr), peerPort);
/* periodic reconnectPulse() deals with the rest... */
return 1;
}
else
{
tr_logAddNamedDbg("LPD", "Cannot serve torrent #%s", hashString);
}
}
return res;
}
/**
* @} */
/**
* @note Since it possible for tr_lpdAnnounceMore to get called from outside the LPD module,
* the function needs to be informed of the externally employed housekeeping interval.
* Further, by setting interval to zero (or negative) the caller may actually disable LPD
* announces on a per-interval basis.
*
* TODO: since this function's been made private and is called by a periodic timer,
* most of the previous paragraph isn't true anymore... we weren't using that functionality
* before. are there cases where we should? if not, should we remove the bells & whistles?
*/
static int tr_lpdAnnounceMore(time_t const now, int const interval)
{
int announcesSent = 0;
if (!tr_isSession(session))
{
return -1;
}
if (tr_sessionAllowsLPD(session))
{
for (auto* tor : session->torrents)
{
int announcePrio = 0;
if (!tr_torrentAllowsLPD(tor))
{
continue;
}
/* issue #3208: prioritize downloads before seeds */
switch (tr_torrentGetActivity(tor))
{
case TR_STATUS_DOWNLOAD:
announcePrio = 1;
break;
case TR_STATUS_SEED:
announcePrio = 2;
break;
default:
break;
}
if (announcePrio > 0 && tor->lpdAnnounceAt <= now)
{
if (tr_lpdSendAnnounce(tor))
{
announcesSent++;
}
tor->lpdAnnounceAt = now + lpd_announceInterval * announcePrio;
break; /* that's enough; for this interval */
}
}
}
/* perform housekeeping for the flood protection mechanism */
{
int const maxAnnounceCap = interval * lpd_announceCapFactor;
if (lpd_unsolicitedMsgCounter < 0)
{
tr_logAddNamedInfo(
"LPD",
"Dropped %d announces in the last interval (max. %d allowed)",
-lpd_unsolicitedMsgCounter,
maxAnnounceCap);
}
lpd_unsolicitedMsgCounter = maxAnnounceCap;
}
return announcesSent;
}
static void on_upkeep_timer([[maybe_unused]] evutil_socket_t s, [[maybe_unused]] short type, [[maybe_unused]] void* user_data)
{
time_t const now = tr_time();
tr_lpdAnnounceMore(now, UPKEEP_INTERVAL_SECS);
tr_timerAdd(upkeep_timer, UPKEEP_INTERVAL_SECS, 0);
}
/**
* @brief Processing of timeout notifications and incoming data on the socket
* @note maximum rate of read events is limited according to @a lpd_maxAnnounceCap
* @see DoS */
static void event_callback([[maybe_unused]] evutil_socket_t s, short type, [[maybe_unused]] void* user_data)
{
TR_ASSERT(tr_isSession(session));
/* do not allow announces to be processed if LPD is disabled */
if (!tr_sessionAllowsLPD(session))
{
return;
}
if ((type & EV_READ) != 0)
{
struct sockaddr_in foreignAddr;
int addrLen = sizeof(foreignAddr);
char foreignMsg[lpd_maxDatagramLength + 1];
/* process local announcement from foreign peer */
int res = recvfrom(
lpd_socket,
foreignMsg,
lpd_maxDatagramLength,
0,
(struct sockaddr*)&foreignAddr,
(socklen_t*)&addrLen);
/* besides, do we get flooded? then bail out! */
if (--lpd_unsolicitedMsgCounter < 0)
{
return;
}
if (res > 0 && res <= lpd_maxDatagramLength)
{
auto foreignPeer = tr_pex{};
/* be paranoid enough about zero terminating the foreign string */
foreignMsg[res] = '\0';
foreignPeer.addr.addr.addr4 = foreignAddr.sin_addr;
if (tr_lpdConsiderAnnounce(&foreignPeer, foreignMsg) != 0)
{
return; /* OK so far, no log message */
}
}
tr_logAddNamedDbg("LPD", "Discarded invalid multicast message");
}
}