1 /* 2 * cachedb/redis.c - cachedb redis module 3 * 4 * Copyright (c) 2018, NLnet Labs. All rights reserved. 5 * 6 * This software is open source. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * Redistributions of source code must retain the above copyright notice, 13 * this list of conditions and the following disclaimer. 14 * 15 * Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * Neither the name of the NLNET LABS nor the names of its contributors may 20 * be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 */ 35 36 /** 37 * \file 38 * 39 * This file contains a module that uses the redis database to cache 40 * dns responses. 41 */ 42 43 #include "config.h" 44 #ifdef USE_CACHEDB 45 #include "cachedb/redis.h" 46 #include "cachedb/cachedb.h" 47 #include "util/alloc.h" 48 #include "util/config_file.h" 49 #include "sldns/sbuffer.h" 50 51 #ifdef USE_REDIS 52 #include "hiredis/hiredis.h" 53 54 struct redis_moddata { 55 redisContext** ctxs; /* thread-specific redis contexts */ 56 int numctxs; /* number of ctx entries */ 57 const char* server_host; /* server's IP address or host name */ 58 int server_port; /* server's TCP port */ 59 const char* server_path; /* server's unix path, or "", NULL if unused */ 60 const char* server_password; /* server's AUTH password, or "", NULL if unused */ 61 struct timeval command_timeout; /* timeout for commands */ 62 struct timeval connect_timeout; /* timeout for connect */ 63 int logical_db; /* the redis logical database to use */ 64 }; 65 66 static redisReply* redis_command(struct module_env*, struct cachedb_env*, 67 const char*, const uint8_t*, size_t); 68 69 static void 70 moddata_clean(struct redis_moddata** moddata) { 71 if(!moddata || !*moddata) 72 return; 73 if((*moddata)->ctxs) { 74 int i; 75 for(i = 0; i < (*moddata)->numctxs; i++) { 76 if((*moddata)->ctxs[i]) 77 redisFree((*moddata)->ctxs[i]); 78 } 79 free((*moddata)->ctxs); 80 } 81 free(*moddata); 82 *moddata = NULL; 83 } 84 85 static redisContext* 86 redis_connect(const struct redis_moddata* moddata) 87 { 88 redisContext* ctx; 89 90 if(moddata->server_path && moddata->server_path[0]!=0) { 91 ctx = redisConnectUnixWithTimeout(moddata->server_path, 92 moddata->connect_timeout); 93 } else { 94 ctx = redisConnectWithTimeout(moddata->server_host, 95 moddata->server_port, moddata->connect_timeout); 96 } 97 if(!ctx || ctx->err) { 98 const char *errstr = "out of memory"; 99 if(ctx) 100 errstr = ctx->errstr; 101 log_err("failed to connect to redis server: %s", errstr); 102 goto fail; 103 } 104 if(redisSetTimeout(ctx, moddata->command_timeout) != REDIS_OK) { 105 log_err("failed to set redis timeout"); 106 goto fail; 107 } 108 if(moddata->server_password && moddata->server_password[0]!=0) { 109 redisReply* rep; 110 rep = redisCommand(ctx, "AUTH %s", moddata->server_password); 111 if(!rep || rep->type == REDIS_REPLY_ERROR) { 112 log_err("failed to authenticate with password"); 113 freeReplyObject(rep); 114 goto fail; 115 } 116 freeReplyObject(rep); 117 } 118 if(moddata->logical_db > 0) { 119 redisReply* rep; 120 rep = redisCommand(ctx, "SELECT %d", moddata->logical_db); 121 if(!rep || rep->type == REDIS_REPLY_ERROR) { 122 log_err("failed to set logical database (%d)", 123 moddata->logical_db); 124 freeReplyObject(rep); 125 goto fail; 126 } 127 freeReplyObject(rep); 128 } 129 verbose(VERB_OPS, "Connection to Redis established"); 130 return ctx; 131 132 fail: 133 if(ctx) 134 redisFree(ctx); 135 return NULL; 136 } 137 138 static int 139 redis_init(struct module_env* env, struct cachedb_env* cachedb_env) 140 { 141 int i; 142 struct redis_moddata* moddata = NULL; 143 144 verbose(VERB_OPS, "Redis initialization"); 145 146 moddata = calloc(1, sizeof(struct redis_moddata)); 147 if(!moddata) { 148 log_err("out of memory"); 149 goto fail; 150 } 151 moddata->numctxs = env->cfg->num_threads; 152 moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 153 if(!moddata->ctxs) { 154 log_err("out of memory"); 155 goto fail; 156 } 157 /* note: server_host is a shallow reference to configured string. 158 * we don't have to free it in this module. */ 159 moddata->server_host = env->cfg->redis_server_host; 160 moddata->server_port = env->cfg->redis_server_port; 161 moddata->server_path = env->cfg->redis_server_path; 162 moddata->server_password = env->cfg->redis_server_password; 163 moddata->command_timeout.tv_sec = env->cfg->redis_timeout / 1000; 164 moddata->command_timeout.tv_usec = 165 (env->cfg->redis_timeout % 1000) * 1000; 166 moddata->connect_timeout.tv_sec = env->cfg->redis_timeout / 1000; 167 moddata->connect_timeout.tv_usec = 168 (env->cfg->redis_timeout % 1000) * 1000; 169 if(env->cfg->redis_command_timeout != 0) { 170 moddata->command_timeout.tv_sec = 171 env->cfg->redis_command_timeout / 1000; 172 moddata->command_timeout.tv_usec = 173 (env->cfg->redis_command_timeout % 1000) * 1000; 174 } 175 if(env->cfg->redis_connect_timeout != 0) { 176 moddata->connect_timeout.tv_sec = 177 env->cfg->redis_connect_timeout / 1000; 178 moddata->connect_timeout.tv_usec = 179 (env->cfg->redis_connect_timeout % 1000) * 1000; 180 } 181 moddata->logical_db = env->cfg->redis_logical_db; 182 for(i = 0; i < moddata->numctxs; i++) { 183 redisContext* ctx = redis_connect(moddata); 184 if(!ctx) { 185 log_err("redis_init: failed to init redis"); 186 goto fail; 187 } 188 moddata->ctxs[i] = ctx; 189 } 190 cachedb_env->backend_data = moddata; 191 if(env->cfg->redis_expire_records) { 192 redisReply* rep = NULL; 193 int redis_reply_type = 0; 194 /** check if setex command is supported */ 195 rep = redis_command(env, cachedb_env, 196 "SETEX __UNBOUND_REDIS_CHECK__ 1 none", NULL, 0); 197 if(!rep) { 198 /** init failed, no response from redis server*/ 199 log_err("redis_init: failed to init redis, the " 200 "redis-expire-records option requires the SETEX command " 201 "(redis >= 2.0.0)"); 202 goto fail; 203 } 204 redis_reply_type = rep->type; 205 freeReplyObject(rep); 206 switch(redis_reply_type) { 207 case REDIS_REPLY_STATUS: 208 break; 209 default: 210 /** init failed, setex command not supported */ 211 log_err("redis_init: failed to init redis, the " 212 "redis-expire-records option requires the SETEX command " 213 "(redis >= 2.0.0)"); 214 goto fail; 215 } 216 } 217 return 1; 218 219 fail: 220 moddata_clean(&moddata); 221 return 0; 222 } 223 224 static void 225 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env) 226 { 227 struct redis_moddata* moddata = (struct redis_moddata*) 228 cachedb_env->backend_data; 229 (void)env; 230 231 verbose(VERB_OPS, "Redis deinitialization"); 232 moddata_clean(&moddata); 233 } 234 235 /* 236 * Send a redis command and get a reply. Unified so that it can be used for 237 * both SET and GET. If 'data' is non-NULL the command is supposed to be 238 * SET and GET otherwise, but the implementation of this function is agnostic 239 * about the semantics (except for logging): 'command', 'data', and 'data_len' 240 * are opaquely passed to redisCommand(). 241 * This function first checks whether a connection with a redis server has 242 * been established; if not it tries to set up a new one. 243 * It returns redisReply returned from redisCommand() or NULL if some low 244 * level error happens. The caller is responsible to check the return value, 245 * if it's non-NULL, it has to free it with freeReplyObject(). 246 */ 247 static redisReply* 248 redis_command(struct module_env* env, struct cachedb_env* cachedb_env, 249 const char* command, const uint8_t* data, size_t data_len) 250 { 251 redisContext* ctx; 252 redisReply* rep; 253 struct redis_moddata* d = (struct redis_moddata*) 254 cachedb_env->backend_data; 255 256 /* We assume env->alloc->thread_num is a unique ID for each thread 257 * in [0, num-of-threads). We could treat it as an error condition 258 * if the assumption didn't hold, but it seems to be a fundamental 259 * assumption throughout the unbound architecture, so we simply assert 260 * it. */ 261 log_assert(env->alloc->thread_num < d->numctxs); 262 ctx = d->ctxs[env->alloc->thread_num]; 263 264 /* If we've not established a connection to the server or we've closed 265 * it on a failure, try to re-establish a new one. Failures will be 266 * logged in redis_connect(). */ 267 if(!ctx) { 268 ctx = redis_connect(d); 269 d->ctxs[env->alloc->thread_num] = ctx; 270 } 271 if(!ctx) 272 return NULL; 273 274 /* Send the command and get a reply, synchronously. */ 275 rep = (redisReply*)redisCommand(ctx, command, data, data_len); 276 if(!rep) { 277 /* Once an error as a NULL-reply is returned the context cannot 278 * be reused and we'll need to set up a new connection. */ 279 log_err("redis_command: failed to receive a reply, " 280 "closing connection: %s", ctx->errstr); 281 redisFree(ctx); 282 d->ctxs[env->alloc->thread_num] = NULL; 283 return NULL; 284 } 285 286 /* Check error in reply to unify logging in that case. 287 * The caller may perform context-dependent checks and logging. */ 288 if(rep->type == REDIS_REPLY_ERROR) 289 log_err("redis: %s resulted in an error: %s", 290 data ? "set" : "get", rep->str); 291 292 return rep; 293 } 294 295 static int 296 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env, 297 char* key, struct sldns_buffer* result_buffer) 298 { 299 redisReply* rep; 300 char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */ 301 int n; 302 int ret = 0; 303 304 verbose(VERB_ALGO, "redis_lookup of %s", key); 305 306 n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key); 307 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 308 log_err("redis_lookup: unexpected failure to build command"); 309 return 0; 310 } 311 312 rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0); 313 if(!rep) 314 return 0; 315 switch(rep->type) { 316 case REDIS_REPLY_NIL: 317 verbose(VERB_ALGO, "redis_lookup: no data cached"); 318 break; 319 case REDIS_REPLY_STRING: 320 verbose(VERB_ALGO, "redis_lookup found %d bytes", 321 (int)rep->len); 322 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) { 323 log_err("redis_lookup: replied data too long: %lu", 324 (size_t)rep->len); 325 break; 326 } 327 sldns_buffer_clear(result_buffer); 328 sldns_buffer_write(result_buffer, rep->str, rep->len); 329 sldns_buffer_flip(result_buffer); 330 ret = 1; 331 break; 332 case REDIS_REPLY_ERROR: 333 break; /* already logged */ 334 default: 335 log_err("redis_lookup: unexpected type of reply for (%d)", 336 rep->type); 337 break; 338 } 339 freeReplyObject(rep); 340 return ret; 341 } 342 343 static void 344 redis_store(struct module_env* env, struct cachedb_env* cachedb_env, 345 char* key, uint8_t* data, size_t data_len, time_t ttl) 346 { 347 redisReply* rep; 348 int n; 349 int set_ttl = (env->cfg->redis_expire_records && 350 (!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0)); 351 /* Supported commands: 352 * - "SET " + key + " %b" 353 * - "SETEX " + key + " " + ttl + " %b" 354 */ 355 char cmdbuf[6+(CACHEDB_HASHSIZE/8)*2+11+3+1]; 356 357 if (!set_ttl) { 358 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len); 359 /* build command to set to a binary safe string */ 360 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key); 361 } else { 362 /* add expired ttl time to redis ttl to avoid premature eviction of key */ 363 ttl += env->cfg->serve_expired_ttl; 364 verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u", 365 key, (int)data_len, (uint32_t)ttl); 366 /* build command to set to a binary safe string */ 367 n = snprintf(cmdbuf, sizeof(cmdbuf), "SETEX %s %u %%b", key, 368 (uint32_t)ttl); 369 } 370 371 372 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 373 log_err("redis_store: unexpected failure to build command"); 374 return; 375 } 376 377 rep = redis_command(env, cachedb_env, cmdbuf, data, data_len); 378 if(rep) { 379 verbose(VERB_ALGO, "redis_store set completed"); 380 if(rep->type != REDIS_REPLY_STATUS && 381 rep->type != REDIS_REPLY_ERROR) { 382 log_err("redis_store: unexpected type of reply (%d)", 383 rep->type); 384 } 385 freeReplyObject(rep); 386 } 387 } 388 389 struct cachedb_backend redis_backend = { "redis", 390 redis_init, redis_deinit, redis_lookup, redis_store 391 }; 392 #endif /* USE_REDIS */ 393 #endif /* USE_CACHEDB */ 394