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