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
moddata_clean(struct redis_moddata ** moddata)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*
redis_connect(const struct redis_moddata * moddata)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
redis_init(struct module_env * env,struct cachedb_env * cachedb_env)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
redis_deinit(struct module_env * env,struct cachedb_env * cachedb_env)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*
redis_command(struct module_env * env,struct cachedb_env * cachedb_env,const char * command,const uint8_t * data,size_t data_len)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
redis_lookup(struct module_env * env,struct cachedb_env * cachedb_env,char * key,struct sldns_buffer * result_buffer)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
redis_store(struct module_env * env,struct cachedb_env * cachedb_env,char * key,uint8_t * data,size_t data_len,time_t ttl)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