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