Support binding the RPC to a Unix socket on *nix platforms (#2574)

* Support binding the RPC to a Unix socket on *nix

This commit adds unix socket support for a RPC. Some refactoring was
required in order to split out the RPC server's address struct from the
normal network address struct used for peers. It would cause
unacceptable overhead to add the unix socket length to the union.

Co-authored-by: Malte Voos <malte@malvo.org>

* add RPC socket mode to control unix socket perms

Unix socket permissions are important to control for security reasons,
and libevent defaults to 0755, which may not be what users want.

Co-authored-by: LaserEyess <LaserEyess@users.noreply.github.com>
Co-authored-by: Malte Voos <malte@malvo.org>
This commit is contained in:
LaserEyess 2022-02-23 16:09:54 -05:00 committed by GitHub
parent 13ad2b58dc
commit 706735ca88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 222 additions and 24 deletions

View File

@ -18,7 +18,7 @@ using namespace std::literals;
namespace
{
auto constexpr my_static = std::array<std::string_view, 389>{ ""sv,
auto constexpr my_static = std::array<std::string_view, 390>{ ""sv,
"activeTorrentCount"sv,
"activity-date"sv,
"activityDate"sv,
@ -291,6 +291,7 @@ auto constexpr my_static = std::array<std::string_view, 389>{ ""sv,
"rpc-host-whitelist-enabled"sv,
"rpc-password"sv,
"rpc-port"sv,
"rpc-socket-mode"sv,
"rpc-url"sv,
"rpc-username"sv,
"rpc-version"sv,

View File

@ -294,6 +294,7 @@ enum
TR_KEY_rpc_host_whitelist_enabled,
TR_KEY_rpc_password,
TR_KEY_rpc_port,
TR_KEY_rpc_socket_mode,
TR_KEY_rpc_url,
TR_KEY_rpc_username,
TR_KEY_rpc_version,

View File

@ -14,10 +14,17 @@
#include <libdeflate.h>
#ifndef _WIN32
#include <sys/un.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
#include <event2/buffer.h>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/http_struct.h> /* TODO: eventually remove this */
#include <event2/listener.h>
#include "transmission.h"
@ -51,12 +58,49 @@ using namespace std::literals;
static char constexpr MyName[] = "RPC Server";
static char constexpr TrUnixSocketPrefix[] = "unix:";
/* The maximum size of a unix socket path is defined per-platform based on sockaddr_un.sun_path.
* On Windows the fallback is the length of the unix prefix. Subtracting one at the end is for
* double counting null terminators from sun_path and TrUnixSocketPrefix. */
#ifdef _WIN32
auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(TrUnixSocketPrefix) };
#else
auto inline constexpr TrUnixAddrStrLen = size_t{ sizeof(((struct sockaddr_un*)nullptr)->sun_path) + sizeof(TrUnixSocketPrefix) -
1 };
#endif
enum tr_rpc_address_type
{
TR_RPC_AF_INET,
TR_RPC_AF_INET6,
TR_RPC_AF_UNIX
};
struct tr_rpc_address
{
tr_rpc_address_type type;
union
{
struct in6_addr;
struct in_addr;
char unixSocketPath[TrUnixAddrStrLen];
} addr;
};
static tr_rpc_address constexpr tr_rpc_inaddr_any = { TR_RPC_AF_INET, { INADDR_ANY } };
#define MY_REALM "Transmission"
#define dbgmsg(...) tr_logAddDeepNamed(MyName, __VA_ARGS__)
static int constexpr DeflateLevel = 6; // medium / default
static bool constexpr tr_rpc_address_is_valid(tr_rpc_address const* a)
{
return a != nullptr && (a->type == TR_RPC_AF_INET || a->type == TR_RPC_AF_INET6 || a->type == TR_RPC_AF_UNIX);
}
/***
****
***/
@ -544,6 +588,108 @@ static auto constexpr ServerStartRetryDelayIncrement = int{ 5 };
static auto constexpr ServerStartRetryDelayStep = int{ 3 };
static auto constexpr ServerStartRetryMaxDelay = int{ 60 };
static char const* tr_rpc_address_to_string(tr_rpc_address const* addr, char* buf, size_t buflen)
{
TR_ASSERT(tr_rpc_address_is_valid(addr));
switch (addr->type)
{
case TR_RPC_AF_INET:
return evutil_inet_ntop(AF_INET, &addr->addr, buf, buflen);
case TR_RPC_AF_INET6:
return evutil_inet_ntop(AF_INET6, &addr->addr, buf, buflen);
case TR_RPC_AF_UNIX:
tr_strlcpy(buf, addr->addr.unixSocketPath, buflen);
return buf;
default:
return nullptr;
}
}
static std::string tr_rpc_address_with_port(tr_rpc_server const* server)
{
tr_rpc_address const* addr = server->bindAddress;
char addr_buf[TrUnixAddrStrLen];
tr_rpc_address_to_string(addr, addr_buf, sizeof(addr_buf));
std::string addr_port_str{ addr_buf };
if (addr->type != TR_RPC_AF_UNIX)
{
addr_port_str.append(":" + std::to_string(tr_rpcGetPort(server)));
}
return addr_port_str;
}
static bool tr_rpc_address_from_string(tr_rpc_address* dst, std::string_view src)
{
if (tr_strvStartsWith(src, TrUnixSocketPrefix))
{
if (src.length() >= TrUnixAddrStrLen)
{
tr_logAddNamedError(
MyName,
_("Unix socket path must be fewer than %zu characters (including \"%s\" prefix)"),
TrUnixAddrStrLen - 1,
TrUnixSocketPrefix);
return false;
}
dst->type = TR_RPC_AF_UNIX;
tr_strlcpy(dst->addr.unixSocketPath, std::string{ src }.c_str(), TrUnixAddrStrLen);
return true;
}
if (evutil_inet_pton(AF_INET, std::string{ src }.c_str(), &dst->addr) == 1)
{
dst->type = TR_RPC_AF_INET;
return true;
}
if (evutil_inet_pton(AF_INET6, std::string{ src }.c_str(), &dst->addr) == 1)
{
dst->type = TR_RPC_AF_INET6;
return true;
}
return false;
}
static bool bindUnixSocket(struct event_base* base, struct evhttp* httpd, char const* path, int socket_mode)
{
#ifdef _WIN32
tr_logAddNamedError(
MyName,
_("Unix sockets are not supported on Windows. Please change \"%s\" in your configuration file."),
TR_KEY_rpc_bind_address);
return false;
#else
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
tr_strlcpy(addr.sun_path, path + strlen(TrUnixSocketPrefix), sizeof(addr.sun_path));
unlink(addr.sun_path);
struct evconnlistener*
lev = evconnlistener_new_bind(base, nullptr, nullptr, LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&addr, sizeof(addr));
if (lev == nullptr)
{
return false;
}
if (chmod(addr.sun_path, (mode_t)socket_mode) != 0)
{
tr_logAddNamedError(MyName, _("Could not set RPC socket mode to %o, defaulting to 755"), socket_mode);
}
return evhttp_bind_listener(httpd, lev) != nullptr;
#endif
}
static void startServer(void* vserver);
static void rpc_server_on_start_retry(evutil_socket_t /*fd*/, short /*type*/, void* context)
@ -587,14 +733,22 @@ static void startServer(void* vserver)
return;
}
struct evhttp* httpd = evhttp_new(server->session->event_base);
struct event_base* base = server->session->event_base;
struct evhttp* httpd = evhttp_new(base);
evhttp_set_allowed_methods(httpd, EVHTTP_REQ_GET | EVHTTP_REQ_POST | EVHTTP_REQ_OPTIONS);
char const* address = tr_rpcGetBindAddress(server);
tr_port const port = server->port;
if (evhttp_bind_socket(httpd, address, port) == -1)
bool const success = server->bindAddress->type == TR_RPC_AF_UNIX ?
bindUnixSocket(base, httpd, address, server->rpc_socket_mode) :
(evhttp_bind_socket(httpd, address, port) != -1);
auto const addr_port_str = tr_rpc_address_with_port(server);
if (!success)
{
evhttp_free(httpd);
@ -602,15 +756,14 @@ static void startServer(void* vserver)
{
int const retry_delay = rpc_server_start_retry(server);
tr_logAddNamedDbg(MyName, "Unable to bind to %s:%d, retrying in %d seconds", address, port, retry_delay);
tr_logAddNamedDbg(MyName, "Unable to bind to %s, retrying in %d seconds", addr_port_str.c_str(), retry_delay);
return;
}
tr_logAddNamedError(
MyName,
"Unable to bind to %s:%d after %d attempts, giving up",
address,
port,
"Unable to bind to %s after %d attempts, giving up",
addr_port_str.c_str(),
ServerStartRetryCount);
}
else
@ -618,7 +771,7 @@ static void startServer(void* vserver)
evhttp_set_gencb(httpd, handle_request, server);
server->httpd = httpd;
tr_logAddNamedDbg(MyName, "Started listening on %s:%d", address, port);
tr_logAddNamedDbg(MyName, "Started listening on %s", addr_port_str.c_str());
}
rpc_server_start_retry_cancel(server);
@ -638,12 +791,16 @@ static void stopServer(tr_rpc_server* server)
}
char const* address = tr_rpcGetBindAddress(server);
int const port = server->port;
server->httpd = nullptr;
evhttp_free(httpd);
tr_logAddNamedDbg(MyName, "Stopped listening on %s:%d", address, port);
if (server->bindAddress->type == TR_RPC_AF_UNIX)
{
unlink(address + strlen(TrUnixSocketPrefix));
}
tr_logAddNamedDbg(MyName, "Stopped listening on %s", tr_rpc_address_with_port(server).c_str());
}
static void onEnabledChanged(void* vserver)
@ -772,6 +929,16 @@ static void tr_rpcSetHostWhitelistEnabled(tr_rpc_server* server, bool isEnabled)
server->isHostWhitelistEnabled = isEnabled;
}
int tr_rpcGetRPCSocketMode(tr_rpc_server const* server)
{
return server->rpc_socket_mode;
}
static void tr_rpcSetRPCSocketMode(tr_rpc_server* server, int socket_mode)
{
server->rpc_socket_mode = socket_mode;
}
/****
***** PASSWORD
****/
@ -817,7 +984,8 @@ bool tr_rpcIsPasswordEnabled(tr_rpc_server const* server)
char const* tr_rpcGetBindAddress(tr_rpc_server const* server)
{
return tr_address_to_string(&server->bindAddress);
static char addr_buf[TrUnixAddrStrLen];
return tr_rpc_address_to_string(server->bindAddress, addr_buf, sizeof(addr_buf));
}
bool tr_rpcGetAntiBruteForceEnabled(tr_rpc_server const* server)
@ -858,7 +1026,7 @@ tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings)
: compressor{ libdeflate_alloc_compressor(DeflateLevel), libdeflate_free_compressor }
, session{ session_in }
{
auto address = tr_address{};
auto address = tr_rpc_address{};
auto boolVal = bool{};
auto i = int64_t{};
auto sv = std::string_view{};
@ -999,39 +1167,57 @@ tr_rpc_server::tr_rpc_server(tr_session* session_in, tr_variant* settings)
tr_rpcSetAntiBruteForceThreshold(this, i);
}
key = TR_KEY_rpc_socket_mode;
if (!tr_variantDictFindInt(settings, key, &i))
{
missing_settings_key(key);
}
else
{
tr_rpcSetRPCSocketMode(this, i);
}
key = TR_KEY_rpc_bind_address;
if (!tr_variantDictFindStrView(settings, key, &sv))
{
missing_settings_key(key);
address = tr_inaddr_any;
address = tr_rpc_inaddr_any;
}
else
{
if (!tr_address_from_string(&address, std::string{ sv }.c_str()))
if (!tr_rpc_address_from_string(&address, sv))
{
tr_logAddNamedError(MyName, _("%" TR_PRIsv " is not a valid address"), TR_PRIsv_ARG(sv));
address = tr_inaddr_any;
tr_logAddNamedError(MyName, _("%" TR_PRIsv " is not a valid address, falling back to 0.0.0.0."), TR_PRIsv_ARG(sv));
address = tr_rpc_inaddr_any;
}
else if (address.type != TR_AF_INET && address.type != TR_AF_INET6)
else if (address.type != TR_RPC_AF_INET && address.type != TR_RPC_AF_INET6 && address.type != TR_RPC_AF_UNIX)
{
tr_logAddNamedError(
MyName,
_("%" TR_PRIsv " is not an IPv4 or IPv6 address. RPC listeners must be IPv4 or IPv6"),
_("%" TR_PRIsv
" is not an IPv4 address, an IPv6 address, or a unix socket path. RPC listeners must be one of the previously mentioned types. Falling back to 0.0.0.0."),
TR_PRIsv_ARG(sv));
address = tr_inaddr_any;
address = tr_rpc_inaddr_any;
}
}
this->bindAddress = address;
if (address.type == TR_RPC_AF_UNIX)
{
this->isWhitelistEnabled = false;
this->isHostWhitelistEnabled = false;
}
this->bindAddress = (tr_rpc_address*)tr_memdup(&address, sizeof(tr_rpc_address));
if (this->isEnabled)
{
tr_logAddNamedInfo(
MyName,
_("Serving RPC and Web requests on %s:%d%s"),
tr_rpcGetBindAddress(this),
(int)this->port,
_("Serving RPC and Web requests on %s%s"),
tr_rpc_address_with_port(this).c_str(),
this->url.c_str());
tr_runInEventThread(session, startServer, this);
@ -1058,4 +1244,7 @@ tr_rpc_server::~tr_rpc_server()
TR_ASSERT(tr_amInEventThread(this->session));
stopServer(this);
tr_free(this->bindAddress);
this->bindAddress = nullptr;
}

View File

@ -21,6 +21,7 @@
struct event;
struct evhttp;
struct tr_variant;
struct tr_rpc_address;
struct libdeflate_compressor;
class tr_rpc_server
@ -43,7 +44,7 @@ public:
std::string whitelistStr;
std::string url;
struct tr_address bindAddress;
struct tr_rpc_address* bindAddress;
struct event* start_retry_timer = nullptr;
struct evhttp* httpd = nullptr;
@ -52,6 +53,8 @@ public:
int antiBruteForceThreshold = 0;
int loginattempts = 0;
int start_retry_counter = 0;
static int constexpr DefaultRpcSocketMode = 0750;
int rpc_socket_mode = DefaultRpcSocketMode;
tr_port port = 0;
@ -82,6 +85,8 @@ bool tr_rpcGetWhitelistEnabled(tr_rpc_server const* server);
void tr_rpcSetWhitelist(tr_rpc_server* server, std::string_view whitelist);
int tr_rpcGetRPCSocketMode(tr_rpc_server const* server);
std::string const& tr_rpcGetWhitelist(tr_rpc_server const* server);
void tr_rpcSetPassword(tr_rpc_server* server, std::string_view password);

View File

@ -372,6 +372,7 @@ void tr_sessionGetDefaultSettings(tr_variant* d)
tr_variantDictAddBool(d, TR_KEY_rpc_host_whitelist_enabled, true);
tr_variantDictAddInt(d, TR_KEY_rpc_port, TR_DEFAULT_RPC_PORT);
tr_variantDictAddStrView(d, TR_KEY_rpc_url, TR_DEFAULT_RPC_URL_STR);
tr_variantDictAddInt(d, TR_KEY_rpc_socket_mode, tr_rpc_server::DefaultRpcSocketMode);
tr_variantDictAddBool(d, TR_KEY_scrape_paused_torrents_enabled, true);
tr_variantDictAddStrView(d, TR_KEY_script_torrent_added_filename, "");
tr_variantDictAddBool(d, TR_KEY_script_torrent_added_enabled, false);
@ -446,6 +447,7 @@ void tr_sessionGetSettings(tr_session const* s, tr_variant* d)
tr_variantDictAddBool(d, TR_KEY_rpc_enabled, tr_sessionIsRPCEnabled(s));
tr_variantDictAddStr(d, TR_KEY_rpc_password, tr_sessionGetRPCPassword(s));
tr_variantDictAddInt(d, TR_KEY_rpc_port, tr_sessionGetRPCPort(s));
tr_variantDictAddInt(d, TR_KEY_rpc_socket_mode, tr_rpcGetRPCSocketMode(s->rpc_server_.get()));
tr_variantDictAddStr(d, TR_KEY_rpc_url, tr_sessionGetRPCUrl(s));
tr_variantDictAddStr(d, TR_KEY_rpc_username, tr_sessionGetRPCUsername(s));
tr_variantDictAddStr(d, TR_KEY_rpc_whitelist, tr_sessionGetRPCWhitelist(s));