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 timeout; /* timeout for connection setup and commands */ 62 int logical_db; /* the redis logical database to use */ 63 }; 64 65 static redisReply* redis_command(struct module_env*, struct cachedb_env*, 66 const char*, const uint8_t*, size_t); 67 68 static void 69 moddata_clean(struct redis_moddata** moddata) { 70 if(!moddata || !*moddata) 71 return; 72 if((*moddata)->ctxs) { 73 int i; 74 for(i = 0; i < (*moddata)->numctxs; i++) { 75 if((*moddata)->ctxs[i]) 76 redisFree((*moddata)->ctxs[i]); 77 } 78 free((*moddata)->ctxs); 79 } 80 free(*moddata); 81 *moddata = NULL; 82 } 83 84 static redisContext* 85 redis_connect(const struct redis_moddata* moddata) 86 { 87 redisContext* ctx; 88 89 if(moddata->server_path && moddata->server_path[0]!=0) { 90 ctx = redisConnectUnixWithTimeout(moddata->server_path, 91 moddata->timeout); 92 } else { 93 ctx = redisConnectWithTimeout(moddata->server_host, 94 moddata->server_port, moddata->timeout); 95 } 96 if(!ctx || ctx->err) { 97 const char *errstr = "out of memory"; 98 if(ctx) 99 errstr = ctx->errstr; 100 log_err("failed to connect to redis server: %s", errstr); 101 goto fail; 102 } 103 if(redisSetTimeout(ctx, moddata->timeout) != REDIS_OK) { 104 log_err("failed to set redis timeout"); 105 goto fail; 106 } 107 if(moddata->server_password && moddata->server_password[0]!=0) { 108 redisReply* rep; 109 rep = redisCommand(ctx, "AUTH %s", moddata->server_password); 110 if(!rep || rep->type == REDIS_REPLY_ERROR) { 111 log_err("failed to authenticate with password"); 112 freeReplyObject(rep); 113 goto fail; 114 } 115 freeReplyObject(rep); 116 } 117 if(moddata->logical_db > 0) { 118 redisReply* rep; 119 rep = redisCommand(ctx, "SELECT %d", moddata->logical_db); 120 if(!rep || rep->type == REDIS_REPLY_ERROR) { 121 log_err("failed to set logical database (%d)", 122 moddata->logical_db); 123 freeReplyObject(rep); 124 goto fail; 125 } 126 freeReplyObject(rep); 127 } 128 verbose(VERB_OPS, "Connection to Redis established"); 129 return ctx; 130 131 fail: 132 if(ctx) 133 redisFree(ctx); 134 return NULL; 135 } 136 137 static int 138 redis_init(struct module_env* env, struct cachedb_env* cachedb_env) 139 { 140 int i; 141 struct redis_moddata* moddata = NULL; 142 143 verbose(VERB_OPS, "Redis initialization"); 144 145 moddata = calloc(1, sizeof(struct redis_moddata)); 146 if(!moddata) { 147 log_err("out of memory"); 148 goto fail; 149 } 150 moddata->numctxs = env->cfg->num_threads; 151 moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 152 if(!moddata->ctxs) { 153 log_err("out of memory"); 154 goto fail; 155 } 156 /* note: server_host is a shallow reference to configured string. 157 * we don't have to free it in this module. */ 158 moddata->server_host = env->cfg->redis_server_host; 159 moddata->server_port = env->cfg->redis_server_port; 160 moddata->server_path = env->cfg->redis_server_path; 161 moddata->server_password = env->cfg->redis_server_password; 162 moddata->timeout.tv_sec = env->cfg->redis_timeout / 1000; 163 moddata->timeout.tv_usec = (env->cfg->redis_timeout % 1000) * 1000; 164 moddata->logical_db = env->cfg->redis_logical_db; 165 for(i = 0; i < moddata->numctxs; i++) { 166 redisContext* ctx = redis_connect(moddata); 167 if(!ctx) { 168 log_err("redis_init: failed to init redis"); 169 goto fail; 170 } 171 moddata->ctxs[i] = ctx; 172 } 173 cachedb_env->backend_data = moddata; 174 if(env->cfg->redis_expire_records) { 175 redisReply* rep = NULL; 176 int redis_reply_type = 0; 177 /** check if setex command is supported */ 178 rep = redis_command(env, cachedb_env, 179 "SETEX __UNBOUND_REDIS_CHECK__ 1 none", NULL, 0); 180 if(!rep) { 181 /** init failed, no response from redis server*/ 182 log_err("redis_init: failed to init redis, the " 183 "redis-expire-records option requires the SETEX command " 184 "(redis >= 2.0.0)"); 185 goto fail; 186 } 187 redis_reply_type = rep->type; 188 freeReplyObject(rep); 189 switch(redis_reply_type) { 190 case REDIS_REPLY_STATUS: 191 break; 192 default: 193 /** init failed, setex command not supported */ 194 log_err("redis_init: failed to init redis, the " 195 "redis-expire-records option requires the SETEX command " 196 "(redis >= 2.0.0)"); 197 goto fail; 198 } 199 } 200 return 1; 201 202 fail: 203 moddata_clean(&moddata); 204 return 0; 205 } 206 207 static void 208 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env) 209 { 210 struct redis_moddata* moddata = (struct redis_moddata*) 211 cachedb_env->backend_data; 212 (void)env; 213 214 verbose(VERB_OPS, "Redis deinitialization"); 215 moddata_clean(&moddata); 216 } 217 218 /* 219 * Send a redis command and get a reply. Unified so that it can be used for 220 * both SET and GET. If 'data' is non-NULL the command is supposed to be 221 * SET and GET otherwise, but the implementation of this function is agnostic 222 * about the semantics (except for logging): 'command', 'data', and 'data_len' 223 * are opaquely passed to redisCommand(). 224 * This function first checks whether a connection with a redis server has 225 * been established; if not it tries to set up a new one. 226 * It returns redisReply returned from redisCommand() or NULL if some low 227 * level error happens. The caller is responsible to check the return value, 228 * if it's non-NULL, it has to free it with freeReplyObject(). 229 */ 230 static redisReply* 231 redis_command(struct module_env* env, struct cachedb_env* cachedb_env, 232 const char* command, const uint8_t* data, size_t data_len) 233 { 234 redisContext* ctx; 235 redisReply* rep; 236 struct redis_moddata* d = (struct redis_moddata*) 237 cachedb_env->backend_data; 238 239 /* We assume env->alloc->thread_num is a unique ID for each thread 240 * in [0, num-of-threads). We could treat it as an error condition 241 * if the assumption didn't hold, but it seems to be a fundamental 242 * assumption throughout the unbound architecture, so we simply assert 243 * it. */ 244 log_assert(env->alloc->thread_num < d->numctxs); 245 ctx = d->ctxs[env->alloc->thread_num]; 246 247 /* If we've not established a connection to the server or we've closed 248 * it on a failure, try to re-establish a new one. Failures will be 249 * logged in redis_connect(). */ 250 if(!ctx) { 251 ctx = redis_connect(d); 252 d->ctxs[env->alloc->thread_num] = ctx; 253 } 254 if(!ctx) 255 return NULL; 256 257 /* Send the command and get a reply, synchronously. */ 258 rep = (redisReply*)redisCommand(ctx, command, data, data_len); 259 if(!rep) { 260 /* Once an error as a NULL-reply is returned the context cannot 261 * be reused and we'll need to set up a new connection. */ 262 log_err("redis_command: failed to receive a reply, " 263 "closing connection: %s", ctx->errstr); 264 redisFree(ctx); 265 d->ctxs[env->alloc->thread_num] = NULL; 266 return NULL; 267 } 268 269 /* Check error in reply to unify logging in that case. 270 * The caller may perform context-dependent checks and logging. */ 271 if(rep->type == REDIS_REPLY_ERROR) 272 log_err("redis: %s resulted in an error: %s", 273 data ? "set" : "get", rep->str); 274 275 return rep; 276 } 277 278 static int 279 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env, 280 char* key, struct sldns_buffer* result_buffer) 281 { 282 redisReply* rep; 283 char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */ 284 int n; 285 int ret = 0; 286 287 verbose(VERB_ALGO, "redis_lookup of %s", key); 288 289 n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key); 290 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 291 log_err("redis_lookup: unexpected failure to build command"); 292 return 0; 293 } 294 295 rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0); 296 if(!rep) 297 return 0; 298 switch(rep->type) { 299 case REDIS_REPLY_NIL: 300 verbose(VERB_ALGO, "redis_lookup: no data cached"); 301 break; 302 case REDIS_REPLY_STRING: 303 verbose(VERB_ALGO, "redis_lookup found %d bytes", 304 (int)rep->len); 305 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) { 306 log_err("redis_lookup: replied data too long: %lu", 307 (size_t)rep->len); 308 break; 309 } 310 sldns_buffer_clear(result_buffer); 311 sldns_buffer_write(result_buffer, rep->str, rep->len); 312 sldns_buffer_flip(result_buffer); 313 ret = 1; 314 break; 315 case REDIS_REPLY_ERROR: 316 break; /* already logged */ 317 default: 318 log_err("redis_lookup: unexpected type of reply for (%d)", 319 rep->type); 320 break; 321 } 322 freeReplyObject(rep); 323 return ret; 324 } 325 326 static void 327 redis_store(struct module_env* env, struct cachedb_env* cachedb_env, 328 char* key, uint8_t* data, size_t data_len, time_t ttl) 329 { 330 redisReply* rep; 331 int n; 332 int set_ttl = (env->cfg->redis_expire_records && 333 (!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0)); 334 /* Supported commands: 335 * - "SET " + key + " %b" 336 * - "SETEX " + key + " " + ttl + " %b" 337 */ 338 char cmdbuf[6+(CACHEDB_HASHSIZE/8)*2+11+3+1]; 339 340 if (!set_ttl) { 341 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len); 342 /* build command to set to a binary safe string */ 343 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key); 344 } else { 345 /* add expired ttl time to redis ttl to avoid premature eviction of key */ 346 ttl += env->cfg->serve_expired_ttl; 347 verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u", 348 key, (int)data_len, (uint32_t)ttl); 349 /* build command to set to a binary safe string */ 350 n = snprintf(cmdbuf, sizeof(cmdbuf), "SETEX %s %u %%b", key, 351 (uint32_t)ttl); 352 } 353 354 355 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 356 log_err("redis_store: unexpected failure to build command"); 357 return; 358 } 359 360 rep = redis_command(env, cachedb_env, cmdbuf, data, data_len); 361 if(rep) { 362 verbose(VERB_ALGO, "redis_store set completed"); 363 if(rep->type != REDIS_REPLY_STATUS && 364 rep->type != REDIS_REPLY_ERROR) { 365 log_err("redis_store: unexpected type of reply (%d)", 366 rep->type); 367 } 368 freeReplyObject(rep); 369 } 370 } 371 372 struct cachedb_backend redis_backend = { "redis", 373 redis_init, redis_deinit, redis_lookup, redis_store 374 }; 375 #endif /* USE_REDIS */ 376 #endif /* USE_CACHEDB */ 377