prefix = ts.string:is_optional():describe("Key prefix"),
username = ts.string:is_optional():describe("Username"),
password = ts.string:is_optional():describe("Password"),
+ -- TLS options
+ ssl = ts.boolean:is_optional():describe("Enable TLS to Redis"),
+ no_ssl_verify = ts.boolean:is_optional():describe("Disable TLS certificate verification"),
+ ssl_ca = ts.string:is_optional():describe("CA certificate file"),
+ ssl_ca_dir = ts.string:is_optional():describe("CA certificates directory"),
+ ssl_cert = ts.string:is_optional():describe("Client certificate file (PEM)"),
+ ssl_key = ts.string:is_optional():describe("Client private key file (PEM)"),
+ sni = ts.string:is_optional():describe("SNI server name override"),
expand_keys = ts.boolean:is_optional():describe("Expand keys"),
sentinels = (ts.string + ts.array_of(ts.string)):is_optional():describe("Sentinel servers"),
sentinel_watch_time = (ts.number + ts.string / lutil.parse_time_interval):is_optional():describe("Sentinel watch time"),
redis_params['password'] = options['password']
end
+ -- TLS passthrough
+ if options['ssl'] ~= nil and redis_params['ssl'] == nil then
+ redis_params['ssl'] = options['ssl'] and true or false
+ end
+ if options['no_ssl_verify'] ~= nil and redis_params['no_ssl_verify'] == nil then
+ redis_params['no_ssl_verify'] = options['no_ssl_verify'] and true or false
+ end
+ if options['ssl_ca'] and not redis_params['ssl_ca'] then
+ redis_params['ssl_ca'] = options['ssl_ca']
+ end
+ if options['ssl_ca_dir'] and not redis_params['ssl_ca_dir'] then
+ redis_params['ssl_ca_dir'] = options['ssl_ca_dir']
+ end
+ if options['ssl_cert'] and not redis_params['ssl_cert'] then
+ redis_params['ssl_cert'] = options['ssl_cert']
+ end
+ if options['ssl_key'] and not redis_params['ssl_key'] then
+ redis_params['ssl_key'] = options['ssl_key']
+ end
+ if options['sni'] and not redis_params['sni'] then
+ redis_params['sni'] = options['sni']
+ end
+
if not redis_params.sentinels and options.sentinels then
redis_params.sentinels = options.sentinels
end
options['dbname'] = redis_params['db']
end
+ -- TLS options
+ if redis_params.ssl ~= nil then options.ssl = redis_params.ssl end
+ if redis_params.no_ssl_verify ~= nil then options.no_ssl_verify = redis_params.no_ssl_verify end
+ if redis_params.ssl_ca then options.ssl_ca = redis_params.ssl_ca end
+ if redis_params.ssl_ca_dir then options.ssl_ca_dir = redis_params.ssl_ca_dir end
+ if redis_params.ssl_cert then options.ssl_cert = redis_params.ssl_cert end
+ if redis_params.ssl_key then options.ssl_key = redis_params.ssl_key end
+ if redis_params.sni then options.sni = redis_params.sni end
+
lutil.debugm(N, task, 'perform request to redis server' ..
' (host=%s, timeout=%s): cmd: %s', ip_addr,
options.timeout, options.cmd)
options['dbname'] = redis_params['db']
end
+ -- TLS options
+ if redis_params.ssl ~= nil then options.ssl = redis_params.ssl end
+ if redis_params.no_ssl_verify ~= nil then options.no_ssl_verify = redis_params.no_ssl_verify end
+ if redis_params.ssl_ca then options.ssl_ca = redis_params.ssl_ca end
+ if redis_params.ssl_ca_dir then options.ssl_ca_dir = redis_params.ssl_ca_dir end
+ if redis_params.ssl_cert then options.ssl_cert = redis_params.ssl_cert end
+ if redis_params.ssl_key then options.ssl_key = redis_params.ssl_key end
+ if redis_params.sni then options.sni = redis_params.sni end
+
lutil.debugm(N, cfg, 'perform taskless request to redis server' ..
' (host=%s, timeout=%s): cmd: %s', options.host:tostring(true),
options.timeout, options.cmd)
opts.dbname = redis_params.db
end
+ -- TLS options
+ if redis_params.ssl ~= nil then opts.ssl = redis_params.ssl end
+ if redis_params.no_ssl_verify ~= nil then opts.no_ssl_verify = redis_params.no_ssl_verify end
+ if redis_params.ssl_ca then opts.ssl_ca = redis_params.ssl_ca end
+ if redis_params.ssl_ca_dir then opts.ssl_ca_dir = redis_params.ssl_ca_dir end
+ if redis_params.ssl_cert then opts.ssl_cert = redis_params.ssl_cert end
+ if redis_params.ssl_key then opts.ssl_key = redis_params.ssl_key end
+ if redis_params.sni then opts.sni = redis_params.sni end
+
if opts.callback then
lutil.debugm(N, 'perform generic async request to redis server' ..
' (host=%s, timeout=%s): cmd: %s, arguments: %s', addr,
opts.dbname = redis_params.db
end
+ -- TLS options
+ if redis_params.ssl ~= nil then opts.ssl = redis_params.ssl end
+ if redis_params.no_ssl_verify ~= nil then opts.no_ssl_verify = redis_params.no_ssl_verify end
+ if redis_params.ssl_ca then opts.ssl_ca = redis_params.ssl_ca end
+ if redis_params.ssl_ca_dir then opts.ssl_ca_dir = redis_params.ssl_ca_dir end
+ if redis_params.ssl_cert then opts.ssl_cert = redis_params.ssl_cert end
+ if redis_params.ssl_key then opts.ssl_key = redis_params.ssl_key end
+ if redis_params.sni then opts.sni = redis_params.sni end
+
if opts.callback then
local ret, conn = rspamd_redis.connect(opts)
if not ret then
#include "cfg_file.h"
#include "contrib/hiredis/hiredis.h"
#include "contrib/hiredis/async.h"
+#include "contrib/hiredis/hiredis_ssl.h"
#include "contrib/hiredis/adapters/libev.h"
#include "cryptobox.h"
#include "logger.h"
std::string db;
std::string username;
std::string password;
+ /* TLS options */
+ bool use_tls = false;
+ bool no_ssl_verify = false;
+ std::string ca_file;
+ std::string ca_dir;
+ std::string cert_file;
+ std::string key_file;
+ std::string sni;
int port;
redis_pool_key_t key;
bool is_unix;
explicit redis_pool_elt(redis_pool *_pool,
const char *_db, const char *_username,
const char *_password,
- const char *_ip, int _port)
+ const char *_ip, int _port,
+ bool _use_tls = false,
+ bool _no_ssl_verify = false,
+ const char *_ca_file = nullptr,
+ const char *_ca_dir = nullptr,
+ const char *_cert_file = nullptr,
+ const char *_key_file = nullptr,
+ const char *_sni = nullptr)
: pool(_pool), ip(_ip), port(_port),
- key(redis_pool_elt::make_key(_db, _username, _password, _ip, _port))
+ key(redis_pool_elt::make_key(_db, _username, _password, _ip, _port,
+ _use_tls, _no_ssl_verify,
+ _ca_file, _ca_dir, _cert_file, _key_file, _sni))
{
is_unix = ip[0] == '.' || ip[0] == '/';
if (_password) {
password = _password;
}
+
+ use_tls = _use_tls;
+ no_ssl_verify = _no_ssl_verify;
+ if (_ca_file) ca_file = _ca_file;
+ if (_ca_dir) ca_dir = _ca_dir;
+ if (_cert_file) cert_file = _cert_file;
+ if (_key_file) key_file = _key_file;
+ if (_sni) sni = _sni;
}
auto new_connection() -> redisAsyncContext *;
}
inline static auto make_key(const char *db, const char *username,
- const char *password, const char *ip, int port) -> redis_pool_key_t
+ const char *password, const char *ip, int port,
+ bool use_tls = false, bool no_ssl_verify = false,
+ const char *ca_file = nullptr, const char *ca_dir = nullptr,
+ const char *cert_file = nullptr, const char *key_file = nullptr,
+ const char *sni = nullptr) -> redis_pool_key_t
{
rspamd_cryptobox_fast_hash_state_t st;
rspamd_cryptobox_fast_hash_update(&st, ip, strlen(ip));
rspamd_cryptobox_fast_hash_update(&st, &port, sizeof(port));
+ /* TLS parameters */
+ rspamd_cryptobox_fast_hash_update(&st, &use_tls, sizeof(use_tls));
+ rspamd_cryptobox_fast_hash_update(&st, &no_ssl_verify, sizeof(no_ssl_verify));
+ if (ca_file) {
+ rspamd_cryptobox_fast_hash_update(&st, ca_file, strlen(ca_file));
+ }
+ if (ca_dir) {
+ rspamd_cryptobox_fast_hash_update(&st, ca_dir, strlen(ca_dir));
+ }
+ if (cert_file) {
+ rspamd_cryptobox_fast_hash_update(&st, cert_file, strlen(cert_file));
+ }
+ if (key_file) {
+ rspamd_cryptobox_fast_hash_update(&st, key_file, strlen(key_file));
+ }
+ if (sni) {
+ rspamd_cryptobox_fast_hash_update(&st, sni, strlen(sni));
+ }
+
return rspamd_cryptobox_fast_hash_final(&st);
}
class redis_pool final {
static constexpr const double default_timeout = 10.0;
- static constexpr const unsigned default_max_conns = 100;
+static constexpr const unsigned default_max_conns = 100;
/* We want to have references integrity */
ankerl::unordered_dense::map<redisAsyncContext *,
auto new_connection(const char *db, const char *username,
const char *password, const char *ip, int port) -> redisAsyncContext *;
+ auto new_connection_ext(const char *db, const char *username,
+ const char *password, const char *ip, int port,
+ bool use_tls, bool no_ssl_verify,
+ const char *ca_file, const char *ca_dir,
+ const char *cert_file, const char *key_file,
+ const char *sni) -> redisAsyncContext *;
+
auto release_connection(redisAsyncContext *ctx,
enum rspamd_redis_pool_release_type how) -> void;
}
}
+static bool ensure_ssl_inited()
+{
+ static bool inited = false;
+ if (!inited) {
+ if (redisInitOpenSSL() == REDIS_OK) {
+ inited = true;
+ }
+ else {
+ /* still try, hiredis might work if already inited elsewhere */
+ inited = true;
+ }
+ }
+ return inited;
+}
+
auto redis_pool_elt::new_connection() -> redisAsyncContext *
{
if (!inactive.empty()) {
conn->ctx->errstr, ip.c_str(), port, nctx);
if (nctx) {
- active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
- db.c_str(), username.c_str(), password.c_str(), nctx));
- active.front()->elt_pos = active.begin();
+ /* If TLS is configured for this element, initiate it now */
+ if (use_tls && !is_unix) {
+ if (ensure_ssl_inited()) {
+ redisSSLContextError ssl_err = REDIS_SSL_CTX_NONE;
+ redisSSLOptions opts{};
+ opts.cacert_filename = ca_file.empty() ? nullptr : ca_file.c_str();
+ opts.capath = ca_dir.empty() ? nullptr : ca_dir.c_str();
+ opts.cert_filename = cert_file.empty() ? nullptr : cert_file.c_str();
+ opts.private_key_filename = key_file.empty() ? nullptr : key_file.c_str();
+ opts.server_name = sni.empty() ? nullptr : sni.c_str();
+ opts.verify_mode = no_ssl_verify ? REDIS_SSL_VERIFY_NONE : REDIS_SSL_VERIFY_PEER;
+
+ auto *ssl_ctx = redisCreateSSLContextWithOptions(&opts, &ssl_err);
+ if (!ssl_ctx || redisInitiateSSLWithContext(&nctx->c, ssl_ctx) != REDIS_OK) {
+ msg_err("cannot start TLS for redis %s:%d: %s", ip.c_str(), port, nctx->errstr);
+ if (ssl_ctx) redisFreeSSLContext(ssl_ctx);
+ redisAsyncFree(nctx);
+ nctx = nullptr;
+ }
+ else {
+ redisFreeSSLContext(ssl_ctx);
+ }
+ }
+ }
+
+ if (nctx) {
+ active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
+ db.c_str(), username.c_str(), password.c_str(), nctx));
+ active.front()->elt_pos = active.begin();
+ }
}
return nctx;
auto *nctx = redis_async_new();
if (nctx) {
- active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
- db.c_str(), username.c_str(), password.c_str(), nctx));
- active.front()->elt_pos = active.begin();
- auto conn = active.front().get();
- msg_debug_rpool("no inactive connections; opened new connection to %s:%d: %p",
- ip.c_str(), port, nctx);
+ /* If TLS is configured for this element, initiate it now */
+ if (use_tls && !is_unix) {
+ if (ensure_ssl_inited()) {
+ redisSSLContextError ssl_err = REDIS_SSL_CTX_NONE;
+ redisSSLOptions opts{};
+ opts.cacert_filename = ca_file.empty() ? nullptr : ca_file.c_str();
+ opts.capath = ca_dir.empty() ? nullptr : ca_dir.c_str();
+ opts.cert_filename = cert_file.empty() ? nullptr : cert_file.c_str();
+ opts.private_key_filename = key_file.empty() ? nullptr : key_file.c_str();
+ opts.server_name = sni.empty() ? nullptr : sni.c_str();
+ opts.verify_mode = no_ssl_verify ? REDIS_SSL_VERIFY_NONE : REDIS_SSL_VERIFY_PEER;
+
+ auto *ssl_ctx = redisCreateSSLContextWithOptions(&opts, &ssl_err);
+ if (!ssl_ctx || redisInitiateSSLWithContext(&nctx->c, ssl_ctx) != REDIS_OK) {
+ msg_err("cannot start TLS for redis %s:%d: %s", ip.c_str(), port, nctx->errstr);
+ if (ssl_ctx) redisFreeSSLContext(ssl_ctx);
+ redisAsyncFree(nctx);
+ nctx = nullptr;
+ }
+ else {
+ redisFreeSSLContext(ssl_ctx);
+ }
+ }
+ }
+
+ if (nctx) {
+ active.emplace_front(std::make_unique<redis_pool_connection>(pool, this,
+ db.c_str(), username.c_str(), password.c_str(), nctx));
+ active.front()->elt_pos = active.begin();
+ auto conn = active.front().get();
+ msg_debug_rpool("no inactive connections; opened new connection to %s:%d: %p",
+ ip.c_str(), port, nctx);
+ }
}
return nctx;
return nullptr;
}
+auto redis_pool::new_connection_ext(const char *db, const char *username,
+ const char *password, const char *ip, int port,
+ bool use_tls, bool no_ssl_verify,
+ const char *ca_file, const char *ca_dir,
+ const char *cert_file, const char *key_file,
+ const char *sni) -> redisAsyncContext *
+{
+
+ if (!wanna_die) {
+ auto key = redis_pool_elt::make_key(db, username, password, ip, port,
+ use_tls, no_ssl_verify, ca_file, ca_dir,
+ cert_file, key_file, sni);
+ auto found_elt = elts_by_key.find(key);
+
+ if (found_elt != elts_by_key.end()) {
+ auto &elt = found_elt->second;
+
+ return elt.new_connection();
+ }
+ else {
+ /* Need to create a pool */
+ auto nelt = elts_by_key.try_emplace(key,
+ this, db, username, password, ip, port,
+ use_tls, no_ssl_verify, ca_file, ca_dir,
+ cert_file, key_file, sni);
+
+ return nelt.first->second.new_connection();
+ }
+ }
+
+ return nullptr;
+}
+
auto redis_pool::release_connection(redisAsyncContext *ctx,
enum rspamd_redis_pool_release_type how) -> void
{
struct redisAsyncContext *
rspamd_redis_pool_connect(void *p,
- const char *db, const char *username,
- const char *password, const char *ip, int port)
+ const char *db, const char *username,
+ const char *password, const char *ip, int port)
{
g_assert(p != NULL);
auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
}
+struct redisAsyncContext *
+rspamd_redis_pool_connect_ext(void *p,
+ const char *db, const char *username,
+ const char *password, const char *ip, int port,
+ const struct rspamd_redis_tls_opts *tls)
+{
+ g_assert(p != NULL);
+ auto *pool = reinterpret_cast<class rspamd::redis_pool *>(p);
+
+ if (tls && tls->use_tls) {
+ return pool->new_connection_ext(db, username, password, ip, port,
+ true, tls->no_ssl_verify != 0,
+ tls->ca_file, tls->ca_dir,
+ tls->cert_file, tls->key_file,
+ tls->sni);
+ }
+ else {
+ return pool->new_connection(db, username, password, ip, port);
+ }
+}
+
+
void rspamd_redis_pool_release_connection(void *p,
struct redisAsyncContext *ctx, enum rspamd_redis_pool_release_type how)
{
#include "contrib/hiredis/hiredis.h"
#include "contrib/hiredis/async.h"
+#include "redis_pool.h"
#define REDIS_DEFAULT_TIMEOUT 1.0
struct rspamd_task *task = NULL;
const char *host = NULL;
const char *username = NULL, *password = NULL, *dbname = NULL, *log_tag = NULL;
+ struct rspamd_redis_tls_opts tls_opts;
int cbref = -1;
struct rspamd_config *cfg = NULL;
struct rspamd_async_session *session = NULL;
gboolean ret = FALSE;
unsigned int flags = 0;
+ memset(&tls_opts, 0, sizeof(tls_opts));
+
if (lua_istable(L, 1)) {
/* Table version */
lua_pushvalue(L, 1);
}
lua_pop(L, 1);
+ /* TLS options (optional) */
+ lua_pushstring(L, "ssl");
+ lua_gettable(L, -2);
+ if (!!lua_toboolean(L, -1)) {
+ tls_opts.use_tls = 1;
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "no_ssl_verify");
+ lua_gettable(L, -2);
+ if (!!lua_toboolean(L, -1)) {
+ tls_opts.no_ssl_verify = 1;
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "ssl_ca");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ tls_opts.ca_file = lua_tostring(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "ssl_ca_dir");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ tls_opts.ca_dir = lua_tostring(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "ssl_cert");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ tls_opts.cert_file = lua_tostring(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "ssl_key");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ tls_opts.key_file = lua_tostring(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "sni");
+ lua_gettable(L, -2);
+ if (lua_type(L, -1) == LUA_TSTRING) {
+ tls_opts.sni = lua_tostring(L, -1);
+ }
+ lua_pop(L, 1);
+
lua_pushstring(L, "no_pool");
lua_gettable(L, -2);
if (!!lua_toboolean(L, -1)) {
if (ret) {
ud->terminated = 0;
- ud->ctx = rspamd_redis_pool_connect(ud->pool,
+ ud->ctx = rspamd_redis_pool_connect_ext(ud->pool,
dbname, username, password,
rspamd_inet_address_to_string(addr->addr),
- rspamd_inet_address_get_port(addr->addr));
+ rspamd_inet_address_get_port(addr->addr),
+ &tls_opts);
if (ip) {
rspamd_inet_address_free(ip);