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 struct timeval timeout; /* timeout for connection setup and commands */ 60 }; 61 62 static redisContext* 63 redis_connect(const struct redis_moddata* moddata) 64 { 65 redisContext* ctx; 66 67 ctx = redisConnectWithTimeout(moddata->server_host, 68 moddata->server_port, moddata->timeout); 69 if(!ctx || ctx->err) { 70 const char *errstr = "out of memory"; 71 if(ctx) 72 errstr = ctx->errstr; 73 log_err("failed to connect to redis server: %s", errstr); 74 goto fail; 75 } 76 if(redisSetTimeout(ctx, moddata->timeout) != REDIS_OK) { 77 log_err("failed to set redis timeout"); 78 goto fail; 79 } 80 return ctx; 81 82 fail: 83 if(ctx) 84 redisFree(ctx); 85 return NULL; 86 } 87 88 static int 89 redis_init(struct module_env* env, struct cachedb_env* cachedb_env) 90 { 91 int i; 92 struct redis_moddata* moddata = NULL; 93 94 verbose(VERB_ALGO, "redis_init"); 95 96 moddata = calloc(1, sizeof(struct redis_moddata)); 97 if(!moddata) { 98 log_err("out of memory"); 99 return 0; 100 } 101 moddata->numctxs = env->cfg->num_threads; 102 moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 103 if(!moddata->ctxs) { 104 log_err("out of memory"); 105 free(moddata); 106 return 0; 107 } 108 /* note: server_host is a shallow reference to configured string. 109 * we don't have to free it in this module. */ 110 moddata->server_host = env->cfg->redis_server_host; 111 moddata->server_port = env->cfg->redis_server_port; 112 moddata->timeout.tv_sec = env->cfg->redis_timeout / 1000; 113 moddata->timeout.tv_usec = (env->cfg->redis_timeout % 1000) * 1000; 114 for(i = 0; i < moddata->numctxs; i++) 115 moddata->ctxs[i] = redis_connect(moddata); 116 cachedb_env->backend_data = moddata; 117 return 1; 118 } 119 120 static void 121 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env) 122 { 123 struct redis_moddata* moddata = (struct redis_moddata*) 124 cachedb_env->backend_data; 125 (void)env; 126 127 verbose(VERB_ALGO, "redis_deinit"); 128 129 if(!moddata) 130 return; 131 if(moddata->ctxs) { 132 int i; 133 for(i = 0; i < moddata->numctxs; i++) { 134 if(moddata->ctxs[i]) 135 redisFree(moddata->ctxs[i]); 136 } 137 free(moddata->ctxs); 138 } 139 free(moddata); 140 } 141 142 /* 143 * Send a redis command and get a reply. Unified so that it can be used for 144 * both SET and GET. If 'data' is non-NULL the command is supposed to be 145 * SET and GET otherwise, but the implementation of this function is agnostic 146 * about the semantics (except for logging): 'command', 'data', and 'data_len' 147 * are opaquely passed to redisCommand(). 148 * This function first checks whether a connection with a redis server has 149 * been established; if not it tries to set up a new one. 150 * It returns redisReply returned from redisCommand() or NULL if some low 151 * level error happens. The caller is responsible to check the return value, 152 * if it's non-NULL, it has to free it with freeReplyObject(). 153 */ 154 static redisReply* 155 redis_command(struct module_env* env, struct cachedb_env* cachedb_env, 156 const char* command, const uint8_t* data, size_t data_len) 157 { 158 redisContext* ctx; 159 redisReply* rep; 160 struct redis_moddata* d = (struct redis_moddata*) 161 cachedb_env->backend_data; 162 163 /* We assume env->alloc->thread_num is a unique ID for each thread 164 * in [0, num-of-threads). We could treat it as an error condition 165 * if the assumption didn't hold, but it seems to be a fundamental 166 * assumption throughout the unbound architecture, so we simply assert 167 * it. */ 168 log_assert(env->alloc->thread_num < d->numctxs); 169 ctx = d->ctxs[env->alloc->thread_num]; 170 171 /* If we've not established a connection to the server or we've closed 172 * it on a failure, try to re-establish a new one. Failures will be 173 * logged in redis_connect(). */ 174 if(!ctx) { 175 ctx = redis_connect(d); 176 d->ctxs[env->alloc->thread_num] = ctx; 177 } 178 if(!ctx) 179 return NULL; 180 181 /* Send the command and get a reply, synchronously. */ 182 rep = (redisReply*)redisCommand(ctx, command, data, data_len); 183 if(!rep) { 184 /* Once an error as a NULL-reply is returned the context cannot 185 * be reused and we'll need to set up a new connection. */ 186 log_err("redis_command: failed to receive a reply, " 187 "closing connection: %s", ctx->errstr); 188 redisFree(ctx); 189 d->ctxs[env->alloc->thread_num] = NULL; 190 return NULL; 191 } 192 193 /* Check error in reply to unify logging in that case. 194 * The caller may perform context-dependent checks and logging. */ 195 if(rep->type == REDIS_REPLY_ERROR) 196 log_err("redis: %s resulted in an error: %s", 197 data ? "set" : "get", rep->str); 198 199 return rep; 200 } 201 202 static int 203 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env, 204 char* key, struct sldns_buffer* result_buffer) 205 { 206 redisReply* rep; 207 char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+1]; /* "GET " + key */ 208 int n; 209 int ret = 0; 210 211 verbose(VERB_ALGO, "redis_lookup of %s", key); 212 213 n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key); 214 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 215 log_err("redis_lookup: unexpected failure to build command"); 216 return 0; 217 } 218 219 rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0); 220 if(!rep) 221 return 0; 222 switch (rep->type) { 223 case REDIS_REPLY_NIL: 224 verbose(VERB_ALGO, "redis_lookup: no data cached"); 225 break; 226 case REDIS_REPLY_STRING: 227 verbose(VERB_ALGO, "redis_lookup found %d bytes", 228 (int)rep->len); 229 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) { 230 log_err("redis_lookup: replied data too long: %lu", 231 (size_t)rep->len); 232 break; 233 } 234 sldns_buffer_clear(result_buffer); 235 sldns_buffer_write(result_buffer, rep->str, rep->len); 236 sldns_buffer_flip(result_buffer); 237 ret = 1; 238 break; 239 case REDIS_REPLY_ERROR: 240 break; /* already logged */ 241 default: 242 log_err("redis_lookup: unexpected type of reply for (%d)", 243 rep->type); 244 break; 245 } 246 freeReplyObject(rep); 247 return ret; 248 } 249 250 static void 251 redis_store(struct module_env* env, struct cachedb_env* cachedb_env, 252 char* key, uint8_t* data, size_t data_len) 253 { 254 redisReply* rep; 255 char cmdbuf[4+(CACHEDB_HASHSIZE/8)*2+3+1]; /* "SET " + key + " %b" */ 256 int n; 257 258 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len); 259 260 /* build command to set to a binary safe string */ 261 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key); 262 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 263 log_err("redis_store: unexpected failure to build command"); 264 return; 265 } 266 267 rep = redis_command(env, cachedb_env, cmdbuf, data, data_len); 268 if(rep) { 269 verbose(VERB_ALGO, "redis_store set completed"); 270 if(rep->type != REDIS_REPLY_STATUS && 271 rep->type != REDIS_REPLY_ERROR) { 272 log_err("redis_store: unexpected type of reply (%d)", 273 rep->type); 274 } 275 freeReplyObject(rep); 276 } 277 } 278 279 struct cachedb_backend redis_backend = { "redis", 280 redis_init, redis_deinit, redis_lookup, redis_store 281 }; 282 #endif /* USE_REDIS */ 283 #endif /* USE_CACHEDB */ 284