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