All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Groups Pages
rlm_redis_ippool.c
Go to the documentation of this file.
1 /*
2  * This program is is free software; you can redistribute it and/or modify
3  * it under the terms of the GNU General Public License as published by
4  * the Free Software Foundation; either version 2 of the License, or (at
5  * your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10  * GNU General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public License
13  * along with this program; if not, write to the Free Software
14  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15  */
16 
17 /**
18  * $Id: 501ad5ec61f5c42c5006ec247b1a562178b7a8d2 $
19  * @file rlm_redis_ippool.c
20  * @brief IP Allocation module with a redis backend.
21  *
22  * @author Arran Cudbard-Bell
23  *
24  * @copyright 2015 Arran Cudbard-Bell <a.cudbardb@freeradius.org>
25  * @copyright 2015 The FreeRADIUS server project
26  */
27 
28 RCSID("$Id: 501ad5ec61f5c42c5006ec247b1a562178b7a8d2 $")
29 
30 #include <freeradius-devel/radiusd.h>
31 #include <freeradius-devel/modules.h>
32 #include <freeradius-devel/modpriv.h>
33 #include <freeradius-devel/rad_assert.h>
34 
35 #include "redis.h"
36 #include "cluster.h"
37 #include "redis_ippool.h"
38 
39 /** rlm_redis module instance
40  *
41  */
42 typedef struct rlm_redis_ippool {
43  fr_redis_conf_t conf; //!< Connection parameters for the Redis server.
44  //!< Must be first field in this struct.
45 
46  char const *name; //!< Instance name.
47 
48  vp_tmpl_t *pool_name; //!< Name of the pool we're allocating IP addresses from.
49 
50  vp_tmpl_t *offer_time; //!< How long we should reserve a lease for during
51  //!< the pre-allocation stage (typically responding
52  //!< to DHCP discover).
53  vp_tmpl_t *lease_time; //!< How long an IP address should be allocated for.
54 
55  uint32_t wait_num; //!< How many slaves we want to acknowledge allocations
56  //!< or updates.
57 
58  struct timeval wait_timeout; //!< How long we wait for slaves to acknowledge writing.
59 
60  vp_tmpl_t *device_id; //!< Unique device identifier. Could be mac-address
61  //!< or a combination of User-Name and something
62  //!< unique to the device.
63 
64  vp_tmpl_t *gateway_id; //!< Gateway identifier, usually
65  //!< NAS-Identifier or the actual Option 82 gateway.
66  //!< Used for bulk lease cleanups.
67 
68  vp_tmpl_t *ip_address; //!< Attribute to read the IP for renewal from.
69  vp_tmpl_t *reply_attr; //!< IP attribute and destination.
70  vp_tmpl_t *range_attr; //!< Attribute to write the range ID to.
71  vp_tmpl_t *expiry_attr; //!< Time at which the lease will expire.
72 
73  bool ipv4_integer; //!< Whether IPv4 addresses should be cast to integers,
74  //!< for renew operations.
75 
76  bool copy_on_update; //!< Copy the address provided by ip_address to the
77  //!< reply_attr if updates are successful.
78 
79  fr_redis_cluster_t *cluster; //!< Redis cluster.
81 
85 };
86 
88  { FR_CONF_OFFSET("pool_name", PW_TYPE_TMPL | PW_TYPE_REQUIRED, rlm_redis_ippool_t, pool_name) },
89 
91  { FR_CONF_OFFSET("gateway", PW_TYPE_TMPL, rlm_redis_ippool_t, gateway_id) },\
92 
93  { FR_CONF_OFFSET("offer_time", PW_TYPE_TMPL, rlm_redis_ippool_t, offer_time) },
94  { FR_CONF_OFFSET("lease_time", PW_TYPE_TMPL | PW_TYPE_REQUIRED, rlm_redis_ippool_t, lease_time) },
95 
96  { FR_CONF_OFFSET("wait_num", PW_TYPE_INTEGER, rlm_redis_ippool_t, wait_num) },
97  { FR_CONF_OFFSET("wait_timeout", PW_TYPE_TIMEVAL, rlm_redis_ippool_t, wait_timeout) },
98 
99  { FR_CONF_OFFSET("ip_address", PW_TYPE_TMPL | PW_TYPE_REQUIRED, rlm_redis_ippool_t, ip_address), .dflt = "%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}", .quote = T_DOUBLE_QUOTED_STRING },
100  { FR_CONF_OFFSET("reply_attr", PW_TYPE_TMPL | PW_TYPE_ATTRIBUTE | PW_TYPE_REQUIRED, rlm_redis_ippool_t, reply_attr), .dflt = "&reply:DHCP-Your-IP-Address", .quote = T_BARE_WORD },
101  { FR_CONF_OFFSET("range_attr", PW_TYPE_TMPL | PW_TYPE_ATTRIBUTE | PW_TYPE_REQUIRED, rlm_redis_ippool_t, range_attr), .dflt = "&reply:Pool-Range", .quote = T_BARE_WORD },
102  { FR_CONF_OFFSET("expiry_attr", PW_TYPE_TMPL | PW_TYPE_ATTRIBUTE, rlm_redis_ippool_t, expiry_attr) },
103 
104  { FR_CONF_OFFSET("ipv4_integer", PW_TYPE_BOOLEAN, rlm_redis_ippool_t, ipv4_integer) },
105  { FR_CONF_OFFSET("copy_on_update", PW_TYPE_BOOLEAN, rlm_redis_ippool_t, copy_on_update), .dflt = "yes", .quote = T_BARE_WORD },
106 
107  /*
108  * Split out to allow conversion to universal ippool module with
109  * minimum of config changes.
110  */
111  { FR_CONF_POINTER("redis", PW_TYPE_SUBSECTION, NULL), .dflt = redis_config },
113 };
114 
115 #define EOL "\n"
116 
117 /** Lua script for allocating new leases
118  *
119  * - KEYS[1] The pool name.
120  * - ARGV[1] Wall time (seconds since epoch).
121  * - ARGV[2] Expires in (seconds).
122  * - ARGV[3] Device identifier (administratively configured).
123  * - ARGV[4] (optional) Gateway identifier.
124  *
125  * Returns @verbatim { <rcode>[, <ip>][, <range>][, <lease time>][, <counter>] } @endverbatim
126  * - IPPOOL_RCODE_SUCCESS lease updated..
127  * - IPPOOL_RCODE_NOT_FOUND lease not found in pool.
128  */
129 static char lua_alloc_cmd[] =
130  "local ip" EOL /* 1 */
131  "local exists" EOL /* 2 */
132 
133  "local pool_key" EOL /* 3 */
134  "local address_key" EOL /* 4 */
135  "local device_key" EOL /* 5 */
136 
137  "pool_key = '{' .. KEYS[1] .. '}:"IPPOOL_POOL_KEY"'" EOL /* 6 */
138  "device_key = '{' .. KEYS[1] .. '}:"IPPOOL_DEVICE_KEY":' .. ARGV[3]" EOL /* 7 */
139 
140  /*
141  * Check to see if the client already has a lease,
142  * and if it does return that.
143  *
144  * The additional sanity checks are to allow for the record
145  * of device/ip binding to persist for longer than the lease.
146  */
147  "exists = redis.call('GET', device_key);" EOL /* 8 */
148  "if exists then" EOL /* 9 */
149  " local expires_in = tonumber(redis.call('ZSCORE', pool_key, exists) - ARGV[1])" EOL /* 10 */
150  " if expires_in > 0 then" EOL /* 11 */
151  " ip = redis.call('HMGET', '{' .. KEYS[1] .. '}:"IPPOOL_ADDRESS_KEY":' .. exists, 'device', 'range', 'counter')" EOL /* 12 */
152  " if ip and (ip[1] == ARGV[3]) then" EOL /* 13 */
153  " return {" STRINGIFY(_IPPOOL_RCODE_SUCCESS) ", exists, ip[2], expires_in, ip[3] }" EOL /* 14 */
154  " end" EOL /* 15 */
155  " end" EOL /* 16 */
156  "end" EOL /* 17 */
157 
158  /*
159  * Else, get the IP address which expired the longest time ago.
160  */
161  "ip = redis.call('ZREVRANGE', pool_key, -1, -1, 'WITHSCORES')" EOL /* 18 */
162  "if not ip or not ip[1] then" EOL /* 19 */
163  " return {" STRINGIFY(_IPPOOL_RCODE_POOL_EMPTY) "}" EOL /* 20 */
164  "end" EOL /* 21 */
165  "if ip[2] >= ARGV[1] then" EOL /* 22 */
166  " return {" STRINGIFY(_IPPOOL_RCODE_POOL_EMPTY) "}" EOL /* 23 */
167  "end" EOL /* 24 */
168  "redis.call('ZADD', pool_key, ARGV[1] + ARGV[2], ip[1])" EOL /* 25 */
169 
170  /*
171  * Set the device/gateway keys
172  */
173  "address_key = '{' .. KEYS[1] .. '}:"IPPOOL_ADDRESS_KEY":' .. ip[1]" EOL /* 26 */
174  "redis.call('HMSET', address_key, 'device', ARGV[3], 'gateway', ARGV[4])" EOL /* 27 */
175  "redis.call('SET', device_key, ip[1])" EOL /* 28 */
176  "redis.call('EXPIRE', device_key, ARGV[2])" EOL /* 29 */
177  "return { " EOL /* 30 */
178  " " STRINGIFY(_IPPOOL_RCODE_SUCCESS) "," EOL /* 31 */
179  " ip[1], " EOL /* 32 */
180  " redis.call('HGET', address_key, 'range'), " EOL /* 33 */
181  " tonumber(ARGV[2]), " EOL /* 34 */
182  " redis.call('HINCRBY', address_key, 'counter', 1)" EOL /* 35 */
183  "}" EOL; /* 36 */
184 static char lua_alloc_digest[(SHA1_DIGEST_LENGTH * 2) + 1];
185 
186 /** Lua script for updating leases
187  *
188  * - KEYS[1] The pool name.
189  * - ARGV[1] Wall time (seconds since epoch).
190  * - ARGV[2] Expires in (seconds).
191  * - ARGV[3] IP address to update.
192  * - ARGV[4] Device identifier.
193  * - ARGV[5] (optional) Gateway identifier.
194  *
195  * Returns @verbatim array { <rcode>[, <range>] } @endverbatim
196  * - IPPOOL_RCODE_SUCCESS lease updated..
197  * - IPPOOL_RCODE_NOT_FOUND lease not found in pool.
198  * - IPPOOL_RCODE_EXPIRED lease has already expired.
199  * - IPPOOL_RCODE_DEVICE_MISMATCH lease was allocated to a different client.
200  */
201 static char lua_update_cmd[] =
202  "local ret" EOL /* 1 */
203  "local found" EOL /* 2 */
204 
205  "local pool_key" EOL /* 3 */
206  "local address_key" EOL /* 4 */
207  "local device_key" EOL /* 5 */
208 
209  /*
210  * We either need to know that the IP was last allocated to the
211  * same device, or that the lease on the IP has NOT expired.
212  */
213  "address_key = '{' .. KEYS[1] .. '}:"IPPOOL_ADDRESS_KEY":' .. ARGV[3]" EOL /* 6 */
214  "found = redis.call('HMGET', address_key, 'range', 'device', 'gateway', 'counter' )" EOL /* 7 */
215  "if not found[1] then" EOL /* 8 */
216  " return {" STRINGIFY(_IPPOOL_RCODE_NOT_FOUND) "}" EOL /* 9 */
217  "end" EOL /* 10 */
218  "if found[2] ~= ARGV[4] then" EOL /* 11 */
219  " return {" STRINGIFY(_IPPOOL_RCODE_DEVICE_MISMATCH) ", found[2]}" EOL /* 12 */
220  "end" EOL /* 13 */
221 
222  /*
223  * Update the expiry time
224  */
225  "pool_key = '{' .. KEYS[1] .. '}:"IPPOOL_POOL_KEY"'" EOL /* 14 */
226  "redis.call('ZADD', pool_key, 'XX', ARGV[1] + ARGV[2], ARGV[3])" EOL /* 15 */
227 
228  /*
229  * The device key should usually exist, but
230  * theoretically, if we were right on the cusp
231  * of a lease being expired, it may have been
232  * removed.
233  */
234  "device_key = '{' .. KEYS[1] .. '}:"IPPOOL_DEVICE_KEY":' .. ARGV[4]" EOL /* 16 */
235  "if redis.call('EXPIRE', device_key, ARGV[2]) == 0 then" EOL /* 17 */
236  " redis.call('SET', device_key, ip[1])" EOL /* 18 */
237  " redis.call('EXPIRE', device_key, ARGV[2])" EOL /* 19 */
238  "end" EOL /* 20 */
239 
240  /*
241  * Update the gateway address
242  */
243  "if ARGV[5] ~= found[3] then" EOL /* 21 */
244  " redis.call('HSET', address_key, 'gateway', ARGV[5])" EOL /* 22 */
245  "end" EOL /* 23 */
246  "return { " STRINGIFY(_IPPOOL_RCODE_SUCCESS) ", found[1], found[4] }"EOL; /* 24 */
247 static char lua_update_digest[(SHA1_DIGEST_LENGTH * 2) + 1];
248 
249 /** Lua script for releasing leases
250  *
251  * - KEYS[1] The pool name.
252  * - ARGV[1] Wall time (seconds since epoch).
253  * - ARGV[2] IP address to release.
254  * - ARGV[3] Client identifier.
255  *
256  * Sets the expiry time to be NOW() - 1 to maximise time between
257  * IP address allocations.
258  *
259  * Returns @verbatim array { <rcode>[, <counter>] } @endverbatim
260  * - IPPOOL_RCODE_SUCCESS lease updated..
261  * - IPPOOL_RCODE_NOT_FOUND lease not found in pool.
262  * - IPPOOL_RCODE_DEVICE_MISMATCH lease was allocated to a different client..
263  */
264 static char lua_release_cmd[] =
265  "local ret" EOL /* 1 */
266  "local found" EOL /* 2 */
267 
268  "local pool_key" EOL /* 3 */
269  "local address_key" EOL /* 4 */
270  "local device_key" EOL /* 5 */
271 
272  /*
273  * Check that the device releasing was the one
274  * the IP address is allocated to.
275  */
276  "address_key = '{' .. KEYS[1] .. '}:"IPPOOL_ADDRESS_KEY":' .. ARGV[2]" EOL /* 6 */
277  "found = redis.call('HGET', address_key, 'device')" EOL /* 7 */
278  "if not found then" EOL /* 8 */
279  " return { " STRINGIFY(_IPPOOL_RCODE_NOT_FOUND) "}" EOL /* 9 */
280  "end" EOL /* 11 */
281  "if found and found ~= ARGV[3] then" EOL /* 12 */
282  " return { " STRINGIFY(_IPPOOL_RCODE_DEVICE_MISMATCH) ", found[2] }" EOL /* 13 */
283  "end" EOL /* 14 */
284 
285  /*
286  * Set expiry time to now() - 1
287  */
288  "pool_key = '{' .. KEYS[1] .. '}:"IPPOOL_POOL_KEY"'" EOL /* 15 */
289  "redis.call('ZADD', pool_key, 'XX', ARGV[1] - 1, ARGV[2])" EOL /* 16 */
290 
291  /*
292  * Remove the association between the device and a lease
293  */
294  "device_key = '{' .. KEYS[1] .. '}:"IPPOOL_DEVICE_KEY":' .. ARGV[3]" EOL /* 17 */
295  "redis.call('DEL', device_key)" EOL /* 18 */
296  "return { " EOL
297  " " STRINGIFY(_IPPOOL_RCODE_SUCCESS) "," EOL /* 19 */
298  " redis.call('HINCRBY', address_key, 'counter', 1) - 1" EOL /* 20 */
299  "}"; /* 21 */
300 static char lua_release_digest[(SHA1_DIGEST_LENGTH * 2) + 1];
301 
302 /** Check the requisite number of slaves replicated the lease info
303  *
304  * @param request The current request.
305  * @param wait_num Number of slaves required.
306  * @param reply we got from the server.
307  * @return
308  * - 0 if enough slaves replicated the data.
309  * - -1 if too few slaves replicated the data, or another error.
310  */
311 static inline int ippool_wait_check(REQUEST *request, uint32_t wait_num, redisReply *reply)
312 {
313  if (!wait_num) return 0;
314 
315  if (reply->type != REDIS_REPLY_INTEGER) {
316  REDEBUG("WAIT result is wrong type, expected integer got %s",
317  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
318  return -1;
319  }
320  if (reply->integer < wait_num) {
321  REDEBUG("Too few slaves acknowledged allocation, needed %i, got %lli",
322  wait_num, reply->integer);
323  return -1;
324  }
325  return 0;
326 }
327 
328 /** Find the pool name we'll be allocating from
329  *
330  * @param out Where to write the pool name.
331  * @param outlen Size of the output buffer.
332  * @param inst This instance of the rlm_redis_ippool module.
333  * @param request The current request.
334  * @return
335  * - <= 0 on error.
336  * - > 0 on success (length of data written to out).
337  */
338 static inline ssize_t ippool_pool_name(uint8_t out[], size_t outlen, rlm_redis_ippool_t *inst, REQUEST *request)
339 {
340  ssize_t slen;
341  uint8_t *out_p = out;
342 
343  slen = tmpl_expand(NULL, (char *)out_p, outlen - (out_p - out), request,
344  inst->pool_name, NULL, NULL);
345  if (slen < 0) {
346  REDEBUG("Failed determining pool name");
347  return -1;
348  }
349  if (is_truncated((size_t)slen, outlen)) {
350  REDEBUG("Pool name too long. Expected %zu bytes, got %zu bytes", outlen, (size_t)slen);
351  return -1;
352  }
353  out_p += slen;
354 
355  return out_p - out;
356 }
357 
358 static void ippool_action_print(REQUEST *request, ippool_action_t action,
359  log_lvl_t lvl,
360  uint8_t const *key_prefix, size_t key_prefix_len,
361  char const *ip_str,
362  uint8_t const *device_id, size_t device_id_len,
363  uint8_t const *gateway_id, size_t gateway_id_len,
364  uint32_t expires)
365 {
366  char *key_prefix_str, *device_str = NULL, *gateway_str = NULL;
367 
368  key_prefix_str = fr_asprint(request, (char const *)key_prefix, key_prefix_len, '"');
369  if (gateway_id) gateway_str = fr_asprint(request, (char const *)gateway_id, gateway_id_len, '"');
370  if (device_id) device_str = fr_asprint(request, (char const *)device_id, device_id_len, '"');
371 
372  switch (action) {
374  RDEBUGX(lvl, "Allocating lease from pool \"%s\"%s%s%s%s%s%s, expires in %us",
375  key_prefix_str,
376  device_str ? ", to \"" : "", device_str ? device_str : "",
377  device_str ? "\"" : "",
378  gateway_str ? ", on \"" : "", gateway_str ? gateway_str : "",
379  gateway_str ? "\"" : "",
380  expires);
381  break;
382 
383  case POOL_ACTION_UPDATE:
384  RDEBUGX(lvl, "Updating %s in pool \"%s\"%s%s%s%s%s%s, expires in %us",
385  ip_str, key_prefix_str,
386  device_str ? ", device \"" : "", device_str ? device_str : "",
387  device_str ? "\"" : "",
388  gateway_str ? ", gateway \"" : "", gateway_str ? gateway_str : "",
389  gateway_str ? "\"" : "",
390  expires);
391  break;
392 
393  case POOL_ACTION_RELEASE:
394  RDEBUGX(lvl, "Releasing %s%s%s%s to pool \"%s\"",
395  ip_str,
396  device_str ? " leased by \"" : "", device_str ? device_str : "",
397  device_str ? "\"" : "",
398  key_prefix_str);
399  break;
400 
401  default:
402  break;
403  }
404 
405  /*
406  * Ordering is important, needs to be LIFO
407  * for proper talloc pool re-use.
408  */
409  talloc_free(key_prefix_str);
410  talloc_free(device_str);
411  talloc_free(gateway_str);
412 }
413 
414 /** Execute a script against Redis cluster
415  *
416  * Handles uploading the script to the server if required.
417  *
418  * @note All replies will be freed on error.
419  *
420  * @param[out] out Where to write Redis reply object resulting from the command.
421  * @param[in] request The current request.
422  * @param[in] cluster configuration.
423  * @param[in] key to use to determine the cluster node.
424  * @param[in] key_len length of the key.
425  * @param[in] wait_num If > 0 wait until this many slaves have replicated the data
426  * from the last command.
427  * @param[in] wait_timeout How long to wait for slaves.
428  * @param[in] digest of script.
429  * @param[in] script to upload.
430  * @param[in] cmd EVALSHA command to execute.
431  * @param[in] ... Arguments for the eval command.
432  * @return status of the command.
433  */
434 static fr_redis_rcode_t ippool_script(redisReply **out, REQUEST *request, fr_redis_cluster_t *cluster,
435  uint8_t const *key, size_t key_len,
436  uint32_t wait_num, uint32_t wait_timeout,
437  char const digest[], char const *script,
438  char const *cmd, ...)
439 {
440  fr_redis_conn_t *conn;
441  redisReply *replies[5]; /* Must be equal to the maximum number of pipelined commands */
442  size_t reply_cnt = 0, i;
443 
445  fr_redis_rcode_t s_ret, status;
446  int pipelined = 0;
447 
448  va_list ap;
449 
450  *out = NULL;
451 
452  va_start(ap, cmd);
453 
454  for (s_ret = fr_redis_cluster_state_init(&state, &conn, cluster, request, key, key_len, false);
455  s_ret == REDIS_RCODE_TRY_AGAIN; /* Continue */
456  s_ret = fr_redis_cluster_state_next(&state, &conn, cluster, request, status, &replies[0])) {
457  va_list copy;
458 
459  RDEBUG3("Calling script 0x%s", digest);
460  va_copy(copy, ap); /* copy or segv */
461  redisvAppendCommand(conn->handle, cmd, copy);
462  va_end(copy);
463  pipelined = 1;
464  if (wait_num) {
465  redisAppendCommand(conn->handle, "WAIT %i %i", wait_num, wait_timeout);
466  pipelined++;
467  }
468  reply_cnt = fr_redis_pipeline_result(&status, replies, sizeof(replies) / sizeof(*replies),
469  conn, pipelined);
470  if (status != REDIS_RCODE_NO_SCRIPT) continue;
471 
472  /*
473  * Last command failed with NOSCRIPT, this means
474  * we have to send the Lua script up to the node
475  * so it can be cached.
476  */
477  RDEBUG3("Loading script 0x%s", digest);
478  redisAppendCommand(conn->handle, "MULTI");
479  redisAppendCommand(conn->handle, "SCRIPT LOAD %s", script);
480  va_copy(copy, ap); /* copy or segv */
481  redisvAppendCommand(conn->handle, cmd, copy);
482  va_end(copy);
483  redisAppendCommand(conn->handle, "EXEC");
484  pipelined = 4;
485  if (wait_num) {
486  redisAppendCommand(conn->handle, "WAIT %i %i", wait_num, wait_timeout);
487  pipelined++;
488  }
489 
490  reply_cnt = fr_redis_pipeline_result(&status, replies, sizeof(replies) / sizeof(*replies),
491  conn, pipelined);
492  if (status == REDIS_RCODE_SUCCESS) {
493  if (RDEBUG_ENABLED3) for (i = 0; i < reply_cnt; i++) {
494  fr_redis_reply_print(L_DBG_LVL_3, replies[i], request, i);
495  }
496 
497  if (replies[3]->type != REDIS_REPLY_ARRAY) {
498  REDEBUG("Bad response to EXEC, expected array got %s",
499  fr_int2str(redis_reply_types, replies[3]->type, "<UNKNOWN>"));
500  error:
501  fr_redis_pipeline_free(replies, reply_cnt);
502  status = REDIS_RCODE_ERROR;
503  goto finish;
504  }
505  if (replies[3]->elements != 2) {
506  REDEBUG("Bad response to EXEC, expected 2 result elements, got %zu",
507  replies[3]->elements);
508  goto error;
509  }
510  if (replies[3]->element[0]->type != REDIS_REPLY_STRING) {
511  REDEBUG("Bad response to SCRIPT LOAD, expected string got %s",
512  fr_int2str(redis_reply_types, replies[3]->element[0]->type, "<UNKNOWN>"));
513  goto error;
514  }
515  if (strcmp(replies[3]->element[0]->str, digest) != 0) {
516  RWDEBUG("Incorrect SHA1 from SCRIPT LOAD, expected %s, got %s",
517  digest, replies[3]->element[0]->str);
518  goto error;
519  }
520  }
521  }
522  if (s_ret != REDIS_RCODE_SUCCESS) goto error;
523 
524  switch (reply_cnt) {
525  case 2: /* EVALSHA with wait */
526  if (ippool_wait_check(request, wait_num, replies[1]) < 0) goto error;
527  fr_redis_reply_free(replies[1]); /* Free the wait response */
528  break;
529 
530  case 1: /* EVALSHA */
531  *out = replies[0];
532  break;
533 
534  case 5: /* LOADSCRIPT + EVALSHA + WAIT */
535  if (ippool_wait_check(request, wait_num, replies[4]) < 0) goto error;
536  fr_redis_reply_free(replies[4]); /* Free the wait response */
537  /* FALL-THROUGH */
538 
539  case 4: /* LOADSCRIPT + EVALSHA */
540  fr_redis_reply_free(replies[2]); /* Free the queued cmd response*/
541  fr_redis_reply_free(replies[1]); /* Free the queued script load response */
542  fr_redis_reply_free(replies[0]); /* Free the queued multi response */
543  *out = replies[3]->element[1];
544  replies[3]->element[1] = NULL; /* Prevent double free */
545  fr_redis_reply_free(replies[3]); /* This works because hiredis checks for NULL elements */
546  break;
547 
548  case 0:
549  break;
550  }
551 
552 finish:
553  va_end(ap);
554  return s_ret;
555 }
556 
557 /** Allocate a new IP address from a pool
558  *
559  */
561  uint8_t const *key_prefix, size_t key_prefix_len,
562  uint8_t const *device_id, size_t device_id_len,
563  uint8_t const *gateway_id, size_t gateway_id_len,
564  uint32_t expires)
565 {
566  struct timeval now;
567  redisReply *reply = NULL;
568 
569  fr_redis_rcode_t status;
571 
572  rad_assert(key_prefix);
573  rad_assert(device_id);
574 
575  gettimeofday(&now, NULL);
576 
577  /*
578  * hiredis doesn't deal well with NULL string pointers
579  */
580  if (!gateway_id) gateway_id = (uint8_t const *)"";
581 
582  status = ippool_script(&reply, request, inst->cluster,
583  key_prefix, key_prefix_len,
584  inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout),
585  lua_alloc_digest, lua_alloc_cmd,
586  "EVALSHA %s 1 %b %u %u %b %b",
588  key_prefix, key_prefix_len,
589  (unsigned int)now.tv_sec, expires,
590  device_id, device_id_len,
591  gateway_id, gateway_id_len);
592  if (status != REDIS_RCODE_SUCCESS) {
593  ret = IPPOOL_RCODE_FAIL;
594  goto finish;
595  }
596 
597  rad_assert(reply);
598  if (reply->type != REDIS_REPLY_ARRAY) {
599  REDEBUG("Expected result to be array got \"%s\"",
600  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
601  ret = IPPOOL_RCODE_FAIL;
602  goto finish;
603  }
604 
605  if (reply->elements == 0) {
606  REDEBUG("Got empty result array");
607  ret = IPPOOL_RCODE_FAIL;
608  goto finish;
609  }
610 
611  /*
612  * Process return code
613  */
614  if (reply->element[0]->type != REDIS_REPLY_INTEGER) {
615  REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])",
616  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
617  ret = IPPOOL_RCODE_FAIL;
618  goto finish;
619  }
620  ret = reply->element[0]->integer;
621  if (ret < 0) goto finish;
622 
623  /*
624  * Process IP address
625  */
626  if (reply->elements > 1) {
627  vp_tmpl_t ip_rhs = {
628  .type = TMPL_TYPE_DATA,
629  .tmpl_data_type = PW_TYPE_STRING
630  };
631  vp_map_t ip_map = {
632  .lhs = inst->reply_attr,
633  .op = T_OP_SET,
634  .rhs = &ip_rhs
635  };
636 
637  switch (reply->element[1]->type) {
638  /*
639  * Destination attribute may not be IPv4, in which case
640  * we want to pre-convert the integer value to an IPv4
641  * address before casting it once more to the type of
642  * the destination attribute.
643  */
644  case REDIS_REPLY_INTEGER:
645  {
646  if (ip_map.lhs->tmpl_da->type != PW_TYPE_IPV4_ADDR) {
647  value_data_t tmp;
648 
649  memset(&tmp, 0, sizeof(tmp));
650 
651  tmp.integer = ntohl((uint32_t)reply->element[1]->integer);
652  tmp.length = sizeof(ip_map.rhs->tmpl_data_value.integer);
653 
654  if (value_data_cast(NULL, &ip_map.rhs->tmpl_data_value, PW_TYPE_IPV4_ADDR,
655  NULL, PW_TYPE_INTEGER, NULL, &tmp)) {
656  REDEBUG("Failed converting integer to IPv4 address: %s", fr_strerror());
657  ret = IPPOOL_RCODE_FAIL;
658  goto finish;
659  }
660  } else {
661  ip_map.rhs->tmpl_data_value.integer = ntohl((uint32_t)reply->element[1]->integer);
662  ip_map.rhs->tmpl_data_length = sizeof(ip_map.rhs->tmpl_data_value.integer);
663  ip_map.rhs->tmpl_data_type = PW_TYPE_INTEGER;
664  }
665  }
666  goto do_ip_map;
667 
668  case REDIS_REPLY_STRING:
669  ip_map.rhs->tmpl_data_value.strvalue = reply->element[1]->str;
670  ip_map.rhs->tmpl_data_length = reply->element[1]->len;
671  ip_map.rhs->tmpl_data_type = PW_TYPE_STRING;
672 
673  do_ip_map:
674  if (map_to_request(request, &ip_map, map_to_vp, NULL) < 0) {
675  ret = IPPOOL_RCODE_FAIL;
676  goto finish;
677  }
678  break;
679 
680  default:
681  REDEBUG("Server returned unexpected type \"%s\" for IP element (result[1])",
682  fr_int2str(redis_reply_types, reply->element[1]->type, "<UNKNOWN>"));
683  ret = IPPOOL_RCODE_FAIL;
684  goto finish;
685  }
686  }
687 
688  /*
689  * Process Range identifier
690  */
691  if (reply->elements > 2) {
692  switch (reply->element[2]->type) {
693  /*
694  * Add range ID to request
695  */
696  case REDIS_REPLY_STRING:
697  {
698  vp_tmpl_t range_rhs = {
699  .name = "",
700  .type = TMPL_TYPE_DATA,
701  .tmpl_data_type = PW_TYPE_STRING,
702  .quote = T_DOUBLE_QUOTED_STRING
703  };
704  vp_map_t range_map = {
705  .lhs = inst->range_attr,
706  .op = T_OP_SET,
707  .rhs = &range_rhs
708  };
709 
710  range_map.rhs->tmpl_data_value.strvalue = reply->element[2]->str;
711  range_map.rhs->tmpl_data_length = reply->element[2]->len;
712  range_map.rhs->tmpl_data_type = PW_TYPE_STRING;
713  if (map_to_request(request, &range_map, map_to_vp, NULL) < 0) {
714  ret = IPPOOL_RCODE_FAIL;
715  goto finish;
716  }
717  }
718  break;
719 
720  case REDIS_REPLY_NIL:
721  break;
722 
723  default:
724  REDEBUG("Server returned unexpected type \"%s\" for range element (result[2])",
725  fr_int2str(redis_reply_types, reply->element[2]->type, "<UNKNOWN>"));
726  ret = IPPOOL_RCODE_FAIL;
727  goto finish;
728  }
729  }
730 
731  /*
732  * Process Expiry time
733  */
734  if (inst->expiry_attr && (reply->elements > 3)) {
735  vp_tmpl_t expiry_rhs = {
736  .name = "",
737  .type = TMPL_TYPE_DATA,
738  .tmpl_data_type = PW_TYPE_STRING,
739  .quote = T_DOUBLE_QUOTED_STRING
740  };
741  vp_map_t expiry_map = {
742  .lhs = inst->expiry_attr,
743  .op = T_OP_SET,
744  .rhs = &expiry_rhs
745  };
746 
747  if (reply->element[3]->type != REDIS_REPLY_INTEGER) {
748  REDEBUG("Server returned unexpected type \"%s\" for expiry element (result[3])",
749  fr_int2str(redis_reply_types, reply->element[3]->type, "<UNKNOWN>"));
750  ret = IPPOOL_RCODE_FAIL;
751  goto finish;
752  }
753 
754  expiry_map.rhs->tmpl_data_value.integer = reply->element[3]->integer;
755  expiry_map.rhs->tmpl_data_length = sizeof(expiry_map.rhs->tmpl_data_value.integer);
756  expiry_map.rhs->tmpl_data_type = PW_TYPE_INTEGER;
757  if (map_to_request(request, &expiry_map, map_to_vp, NULL) < 0) {
758  ret = IPPOOL_RCODE_FAIL;
759  goto finish;
760  }
761  }
762 finish:
763  fr_redis_reply_free(reply);
764  return ret;
765 }
766 
767 /** Update an existing IP address in a pool
768  *
769  */
771  uint8_t const *key_prefix, size_t key_prefix_len,
772  fr_ipaddr_t *ip,
773  uint8_t const *device_id, size_t device_id_len,
774  uint8_t const *gateway_id, size_t gateway_id_len,
775  uint32_t expires)
776 {
777  struct timeval now;
778  redisReply *reply = NULL;
779 
780  fr_redis_rcode_t status;
782 
783  vp_tmpl_t range_rhs = { .name = "", .type = TMPL_TYPE_DATA, .tmpl_data_type = PW_TYPE_STRING, .quote = T_DOUBLE_QUOTED_STRING };
784  vp_map_t range_map = { .lhs = inst->range_attr, .op = T_OP_SET, .rhs = &range_rhs };
785 
786  gettimeofday(&now, NULL);
787 
788  /*
789  * hiredis doesn't deal well with NULL string pointers
790  */
791  if (!device_id) device_id = (uint8_t const *)"";
792  if (!gateway_id) gateway_id = (uint8_t const *)"";
793 
794  if ((ip->af == AF_INET) && inst->ipv4_integer) {
795  status = ippool_script(&reply, request, inst->cluster,
796  key_prefix, key_prefix_len,
797  inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout),
798  lua_update_digest, lua_update_cmd,
799  "EVALSHA %s 1 %b %u %u %u %b %b",
801  key_prefix, key_prefix_len,
802  (unsigned int)now.tv_sec, expires,
803  htonl(ip->ipaddr.ip4addr.s_addr),
804  device_id, device_id_len,
805  gateway_id, gateway_id_len);
806  } else {
807  char ip_buff[FR_IPADDR_PREFIX_STRLEN];
808 
809  IPPOOL_SPRINT_IP(ip_buff, ip, ip->prefix);
810  status = ippool_script(&reply, request, inst->cluster,
811  key_prefix, key_prefix_len,
812  inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout),
813  lua_update_digest, lua_update_cmd,
814  "EVALSHA %s 1 %b %u %u %s %b %b",
816  key_prefix, key_prefix_len,
817  (unsigned int)now.tv_sec, expires,
818  ip_buff,
819  device_id, device_id_len,
820  gateway_id, gateway_id_len);
821  }
822  if (status != REDIS_RCODE_SUCCESS) {
823  ret = IPPOOL_RCODE_FAIL;
824  goto finish;
825  }
826 
827  if (reply->type != REDIS_REPLY_ARRAY) {
828  REDEBUG("Expected result to be array got \"%s\"",
829  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
830  ret = IPPOOL_RCODE_FAIL;
831  goto finish;
832  }
833 
834  if (reply->elements == 0) {
835  REDEBUG("Got empty result array");
836  ret = IPPOOL_RCODE_FAIL;
837  goto finish;
838  }
839 
840  /*
841  * Process return code
842  */
843  if (reply->element[0]->type != REDIS_REPLY_INTEGER) {
844  REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])",
845  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
846  ret = IPPOOL_RCODE_FAIL;
847  goto finish;
848  }
849  ret = reply->element[0]->integer;
850  if (ret < 0) goto finish;
851 
852  /*
853  * Process Range identifier
854  */
855  if (reply->elements > 1) {
856  switch (reply->element[1]->type) {
857  /*
858  * Add range ID to request
859  */
860  case REDIS_REPLY_STRING:
861  range_map.rhs->tmpl_data_value.strvalue = reply->element[1]->str;
862  range_map.rhs->tmpl_data_length = reply->element[1]->len;
863  range_map.rhs->tmpl_data_type = PW_TYPE_STRING;
864  if (map_to_request(request, &range_map, map_to_vp, NULL) < 0) {
865  ret = IPPOOL_RCODE_FAIL;
866  goto finish;
867  }
868  break;
869 
870  case REDIS_REPLY_NIL:
871  break;
872 
873  default:
874  REDEBUG("Server returned unexpected type \"%s\" for range element (result[1])",
875  fr_int2str(redis_reply_types, reply->element[0]->type, "<UNKNOWN>"));
876  ret = IPPOOL_RCODE_FAIL;
877  goto finish;
878  }
879  }
880 
881  /*
882  * Copy expiry time to expires attribute (if set)
883  */
884  if (inst->expiry_attr) {
885  vp_tmpl_t expiry_rhs = {
886  .name = "",
887  .type = TMPL_TYPE_DATA,
888  .tmpl_data_type = PW_TYPE_STRING,
889  .quote = T_DOUBLE_QUOTED_STRING
890  };
891  vp_map_t expiry_map = {
892  .lhs = inst->expiry_attr,
893  .op = T_OP_SET,
894  .rhs = &expiry_rhs
895  };
896 
897  expiry_map.rhs->tmpl_data_value.integer = expires;
898  expiry_map.rhs->tmpl_data_length = sizeof(expiry_map.rhs->tmpl_data_value.integer);
899  expiry_map.rhs->tmpl_data_type = PW_TYPE_INTEGER;
900  if (map_to_request(request, &expiry_map, map_to_vp, NULL) < 0) {
901  ret = IPPOOL_RCODE_FAIL;
902  goto finish;
903  }
904  }
905 
906 finish:
907  fr_redis_reply_free(reply);
908 
909  return ret;
910 }
911 
912 /** Release an existing IP address in a pool
913  *
914  */
916  uint8_t const *key_prefix, size_t key_prefix_len,
917  fr_ipaddr_t *ip,
918  uint8_t const *device_id, size_t device_id_len)
919 {
920  struct timeval now;
921  redisReply *reply = NULL;
922 
923  fr_redis_rcode_t status;
925 
926  gettimeofday(&now, NULL);
927 
928  /*
929  * hiredis doesn't deal well with NULL string pointers
930  */
931  if (!device_id) device_id = (uint8_t const *)"";
932 
933  if ((ip->af == AF_INET) && inst->ipv4_integer) {
934  status = ippool_script(&reply, request, inst->cluster,
935  key_prefix, key_prefix_len,
936  inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout),
937  lua_release_digest, lua_release_cmd,
938  "EVALSHA %s 1 %b %u %u %b",
940  key_prefix, key_prefix_len,
941  (unsigned int)now.tv_sec,
942  htonl(ip->ipaddr.ip4addr.s_addr),
943  device_id, device_id_len);
944  } else {
945  char ip_buff[FR_IPADDR_PREFIX_STRLEN];
946 
947  IPPOOL_SPRINT_IP(ip_buff, ip, ip->prefix);
948  status = ippool_script(&reply, request, inst->cluster,
949  key_prefix, key_prefix_len,
950  inst->wait_num, FR_TIMEVAL_TO_MS(&inst->wait_timeout),
951  lua_release_digest, lua_release_cmd,
952  "EVALSHA %s 1 %b %u %s %b",
954  key_prefix, key_prefix_len,
955  (unsigned int)now.tv_sec,
956  ip_buff,
957  device_id, device_id_len);
958  }
959  if (status != REDIS_RCODE_SUCCESS) {
960  ret = IPPOOL_RCODE_FAIL;
961  goto finish;
962  }
963 
964  if (reply->type != REDIS_REPLY_ARRAY) {
965  REDEBUG("Expected result to be array got \"%s\"",
966  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
967  ret = IPPOOL_RCODE_FAIL;
968  goto finish;
969  }
970 
971  if (reply->elements == 0) {
972  REDEBUG("Got empty result array");
973  ret = IPPOOL_RCODE_FAIL;
974  goto finish;
975  }
976 
977  /*
978  * Process return code
979  */
980  if (reply->element[0]->type != REDIS_REPLY_INTEGER) {
981  REDEBUG("Server returned unexpected type \"%s\" for rcode element (result[0])",
982  fr_int2str(redis_reply_types, reply->type, "<UNKNOWN>"));
983  ret = IPPOOL_RCODE_FAIL;
984  goto finish;
985  }
986  ret = reply->element[0]->integer;
987  if (ret < 0) goto finish;
988 
989 finish:
990  fr_redis_reply_free(reply);
991 
992  return ret;
993 }
994 
996 {
997  uint8_t key_prefix[IPPOOL_MAX_KEY_PREFIX_SIZE], device_id_buff[256], gateway_id_buff[256];
998  uint8_t const *device_id = NULL, *gateway_id = NULL;
999  size_t key_prefix_len, device_id_len = 0, gateway_id_len = 0;
1000  ssize_t slen;
1001  fr_ipaddr_t ip;
1002  char expires_buff[20];
1003  char const *expires_str;
1004  unsigned long expires = 0;
1005  char *q;
1006 
1007  slen = ippool_pool_name((uint8_t *)&key_prefix, sizeof(key_prefix), inst, request);
1008  if (slen < 0) return RLM_MODULE_FAIL;
1009 
1010  key_prefix_len = (size_t)slen;
1011 
1012  if (inst->device_id) {
1013  slen = tmpl_expand((char const **)&device_id,
1014  (char *)&device_id_buff, sizeof(device_id_buff),
1015  request, inst->device_id, NULL, NULL);
1016  if (slen < 0) {
1017  REDEBUG("Failed expanding device (%s)", inst->device_id->name);
1018  return RLM_MODULE_FAIL;
1019  }
1020  device_id_len = (size_t)slen;
1021  }
1022 
1023  if (inst->gateway_id) {
1024  slen = tmpl_expand((char const **)&gateway_id,
1025  (char *)&gateway_id_buff, sizeof(gateway_id_buff),
1026  request, inst->gateway_id, NULL, NULL);
1027  if (slen < 0) {
1028  REDEBUG("Failed expanding gateway (%s)", inst->gateway_id->name);
1029  return RLM_MODULE_FAIL;
1030  }
1031  gateway_id_len = (size_t)slen;
1032  }
1033 
1034  switch (action) {
1035  case POOL_ACTION_ALLOCATE:
1036  if (tmpl_expand(&expires_str, expires_buff, sizeof(expires_buff),
1037  request, inst->offer_time, NULL, NULL) < 0) {
1038  REDEBUG("Failed expanding offer_time (%s)", inst->offer_time->name);
1039  return RLM_MODULE_FAIL;
1040  }
1041 
1042  expires = strtoul(expires_str, &q, 10);
1043  if (q != (expires_str + strlen(expires_str))) {
1044  REDEBUG("Invalid offer_time. Must be an integer value");
1045  return RLM_MODULE_FAIL;
1046  }
1047 
1048  ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len, NULL,
1049  device_id, device_id_len, gateway_id, gateway_id_len, expires);
1050  switch (redis_ippool_allocate(inst, request, key_prefix, key_prefix_len,
1051  device_id, device_id_len,
1052  gateway_id, gateway_id_len, (uint32_t)expires)) {
1053  case IPPOOL_RCODE_SUCCESS:
1054  RDEBUG2("IP address lease allocated");
1055  return RLM_MODULE_UPDATED;
1056 
1058  RWDEBUG("Pool contains no free addresses");
1059  return RLM_MODULE_NOTFOUND;
1060 
1061  default:
1062  return RLM_MODULE_FAIL;
1063  }
1064 
1065  case POOL_ACTION_UPDATE:
1066  {
1067  char ip_buff[INET6_ADDRSTRLEN + 4];
1068  char const *ip_str;
1069 
1070  if (tmpl_expand(&expires_str, expires_buff, sizeof(expires_buff),
1071  request, inst->lease_time, NULL, NULL) < 0) {
1072  REDEBUG("Failed expanding lease_time (%s)", inst->lease_time->name);
1073  return RLM_MODULE_FAIL;
1074  }
1075 
1076  expires = strtoul(expires_str, &q, 10);
1077  if (q != (expires_str + strlen(expires_str))) {
1078  REDEBUG("Invalid expires. Must be an integer value");
1079  return RLM_MODULE_FAIL;
1080  }
1081 
1082  if (tmpl_expand(&ip_str, ip_buff, sizeof(ip_buff), request, inst->ip_address, NULL, NULL) < 0) {
1083  REDEBUG("Failed expanding ip_address");
1084  return RLM_MODULE_FAIL;
1085  }
1086 
1087  if (fr_inet_pton(&ip, ip_str, -1, AF_UNSPEC, false, true) < 0) {
1088  REDEBUG("%s", fr_strerror());
1089  return RLM_MODULE_FAIL;
1090  }
1091 
1092  ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len,
1093  ip_str, device_id, device_id_len, gateway_id, gateway_id_len, expires);
1094  switch (redis_ippool_update(inst, request, key_prefix, key_prefix_len,
1095  &ip, device_id, device_id_len,
1096  gateway_id, gateway_id_len, (uint32_t)expires)) {
1097  case IPPOOL_RCODE_SUCCESS:
1098  RDEBUG2("IP address lease updated");
1099 
1100  /*
1101  * Copy over the input IP address to the reply attribute
1102  */
1103  if (inst->copy_on_update) {
1104  vp_tmpl_t ip_rhs = {
1105  .name = "",
1106  .type = TMPL_TYPE_DATA,
1107  .quote = T_BARE_WORD,
1108  };
1109  vp_map_t ip_map = {
1110  .lhs = inst->reply_attr,
1111  .op = T_OP_SET,
1112  .rhs = &ip_rhs
1113  };
1114 
1115  ip_rhs.tmpl_data_length = strlen(ip_str);
1116  ip_rhs.tmpl_data_value.strvalue = ip_str;
1117  ip_rhs.tmpl_data_type = PW_TYPE_STRING;
1118 
1119  if (map_to_request(request, &ip_map, map_to_vp, NULL) < 0) return RLM_MODULE_FAIL;
1120  }
1121  return RLM_MODULE_UPDATED;
1122 
1123  /*
1124  * It's useful to be able to identify the 'not found' case
1125  * as we can relay to a server where the IP address might
1126  * be found. This extremely useful for migrations.
1127  */
1129  REDEBUG("IP address is not a member of the specified pool");
1130  return RLM_MODULE_NOTFOUND;
1131 
1132  case IPPOOL_RCODE_EXPIRED:
1133  REDEBUG("IP address lease already expired at time of renewal");
1134  return RLM_MODULE_INVALID;
1135 
1137  REDEBUG("IP address lease allocated to another device");
1138  return RLM_MODULE_INVALID;
1139 
1140  default:
1141  return RLM_MODULE_FAIL;
1142  }
1143  }
1144 
1145  case POOL_ACTION_RELEASE:
1146  {
1147  char ip_buff[INET6_ADDRSTRLEN + 4];
1148  char const *ip_str;
1149 
1150  if (tmpl_expand(&ip_str, ip_buff, sizeof(ip_buff), request, inst->ip_address, NULL, NULL) < 0) {
1151  REDEBUG("Failed expanding ip_address (%s)", inst->ip_address->name);
1152  return RLM_MODULE_FAIL;
1153  }
1154 
1155  if (fr_inet_pton(&ip, ip_str, -1, AF_UNSPEC, false, true) < 0) {
1156  REDEBUG("%s", fr_strerror());
1157  return RLM_MODULE_FAIL;
1158  }
1159 
1160  ippool_action_print(request, action, L_DBG_LVL_2, key_prefix, key_prefix_len,
1161  ip_str, device_id, device_id_len, gateway_id, gateway_id_len, 0);
1162  switch (redis_ippool_release(inst, request, key_prefix, key_prefix_len,
1163  &ip, device_id, device_id_len)) {
1164  case IPPOOL_RCODE_SUCCESS:
1165  RDEBUG2("IP address released");
1166  return RLM_MODULE_UPDATED;
1167 
1168  /*
1169  * It's useful to be able to identify the 'not found' case
1170  * as we can relay to a server where the IP address might
1171  * be found. This extremely useful for migrations.
1172  */
1174  REDEBUG("IP address is not a member of the specified pool");
1175  return RLM_MODULE_NOTFOUND;
1176 
1178  REDEBUG("IP address lease allocated to another device");
1179  return RLM_MODULE_INVALID;
1180 
1181  default:
1182  return RLM_MODULE_FAIL;
1183  }
1184  }
1185 
1187  RDEBUG2("Bulk release not yet implemented");
1188  return RLM_MODULE_NOOP;
1189 
1190  default:
1191  rad_assert(0);
1192  return RLM_MODULE_FAIL;
1193  }
1194 }
1195 
1196 static rlm_rcode_t mod_accounting(void *instance, REQUEST *request) CC_HINT(nonnull);
1197 static rlm_rcode_t mod_accounting(void *instance, REQUEST *request)
1198 {
1199  rlm_redis_ippool_t *inst = instance;
1200  VALUE_PAIR *vp;
1201 
1202  /*
1203  * Pool-Action override
1204  */
1205  vp = fr_pair_find_by_num(request->config, 0, PW_POOL_ACTION, TAG_ANY);
1206  if (vp) return mod_action(inst, request, vp->vp_integer);
1207 
1208  /*
1209  * Otherwise, guess the action by Acct-Status-Type
1210  */
1211  vp = fr_pair_find_by_num(request->packet->vps, 0, PW_ACCT_STATUS_TYPE, TAG_ANY);
1212  if (!vp) {
1213  RDEBUG2("Couldn't find &request:Acct-Status-Type or &control:Pool-Action, doing nothing...");
1214  return RLM_MODULE_NOOP;
1215  }
1216 
1217  switch (vp->vp_integer) {
1218  case PW_STATUS_START:
1219  case PW_STATUS_ALIVE:
1220  return mod_action(inst, request, POOL_ACTION_UPDATE);
1221 
1222  case PW_STATUS_STOP:
1223  return mod_action(inst, request, POOL_ACTION_RELEASE);
1224 
1227  return mod_action(inst, request, POOL_ACTION_BULK_RELEASE);
1228 
1229  default:
1230  return RLM_MODULE_NOOP;
1231  }
1232 }
1233 
1234 static rlm_rcode_t mod_authorize(void *instance, REQUEST *request) CC_HINT(nonnull);
1235 static rlm_rcode_t mod_authorize(void *instance, REQUEST *request)
1236 {
1237  rlm_redis_ippool_t *inst = instance;
1238  VALUE_PAIR *vp;
1239 
1240  /*
1241  * Unless it's overridden the default action is to allocate
1242  * when called in Post-Auth.
1243  */
1244  vp = fr_pair_find_by_num(request->config, 0, PW_POOL_ACTION, TAG_ANY);
1245  return mod_action(inst, request, vp ? vp->vp_integer : POOL_ACTION_ALLOCATE);
1246 }
1247 
1248 static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request) CC_HINT(nonnull);
1249 static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request)
1250 {
1251  rlm_redis_ippool_t *inst = instance;
1252  VALUE_PAIR *vp;
1253 
1254  /*
1255  * Unless it's overridden the default action is to allocate
1256  * when called in Post-Auth.
1257  */
1258  vp = fr_pair_find_by_num(request->config, 0, PW_POOL_ACTION, TAG_ANY);
1259  return mod_action(inst, request, vp ? vp->vp_integer : POOL_ACTION_ALLOCATE);
1260 }
1261 
1262 static int mod_bootstrap(CONF_SECTION *conf, void *instance)
1263 {
1264  rlm_redis_ippool_t *inst = instance;
1265 
1267 
1268  inst->name = cf_section_name2(conf);
1269  if (!inst->name) inst->name = cf_section_name1(conf);
1270  inst->conf.prefix = talloc_asprintf(inst, "rlm_redis (%s)", inst->name);
1271 
1272  return 0;
1273 }
1274 
1275 static int mod_instantiate(CONF_SECTION *conf, void *instance)
1276 {
1277  static bool done_hash = false;
1278  CONF_SECTION *subcs = cf_subsection_find_next(conf, NULL, "redis");
1279 
1280  rlm_redis_ippool_t *inst = instance;
1281 
1283  rad_assert(subcs);
1284 
1285  inst->cluster = fr_redis_cluster_alloc(inst, subcs, &inst->conf);
1286  if (!inst->cluster) return -1;
1287 
1288  if (!fr_redis_cluster_min_version(inst->cluster, "3.0.2")) {
1289  ERROR("%s", fr_strerror());
1290  return -1;
1291  }
1292 
1293  /*
1294  * Pre-Compute the SHA1 hashes of the Lua scripts
1295  */
1296  if (!done_hash) {
1297  fr_sha1_ctx sha1_ctx;
1298  uint8_t digest[SHA1_DIGEST_LENGTH];
1299 
1300  fr_sha1_init(&sha1_ctx);
1301  fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_alloc_cmd, sizeof(lua_alloc_cmd) - 1);
1302  fr_sha1_final(digest, &sha1_ctx);
1303  fr_bin2hex(lua_alloc_digest, digest, sizeof(digest));
1304 
1305  fr_sha1_init(&sha1_ctx);
1306  fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_update_cmd, sizeof(lua_update_cmd) - 1);
1307  fr_sha1_final(digest, &sha1_ctx);
1308  fr_bin2hex(lua_update_digest, digest, sizeof(digest));
1309 
1310  fr_sha1_init(&sha1_ctx);
1311  fr_sha1_update(&sha1_ctx, (uint8_t const *)lua_release_cmd, sizeof(lua_release_cmd) - 1);
1312  fr_sha1_final(digest, &sha1_ctx);
1313  fr_bin2hex(lua_release_digest, digest, sizeof(digest));
1314  }
1315 
1316  /*
1317  * If we don't have a separate time specifically for offers
1318  * just use the lease time.
1319  */
1320  if (!inst->offer_time) inst->offer_time = inst->lease_time;
1321 
1322  return 0;
1323 }
1324 
1325 extern module_t rlm_redis_ippool;
1326 module_t rlm_redis_ippool = {
1328  .name = "redis",
1329  .type = RLM_TYPE_THREAD_SAFE,
1330  .inst_size = sizeof(rlm_redis_ippool_t),
1331  .config = module_config,
1332  .bootstrap = mod_bootstrap,
1333  .instantiate = mod_instantiate,
1334  .methods = {
1338  },
1339 };
static rlm_rcode_t mod_authorize(void *instance, REQUEST *request) CC_HINT(nonnull)
void fr_sha1_update(fr_sha1_ctx *context, uint8_t const *data, size_t len)
Definition: sha1.c:106
ssize_t tmpl_expand(char const **out, char *buff, size_t outlen, REQUEST *request, vp_tmpl_t const *vpt, xlat_escape_t escape, void *escape_ctx)
Expand a vp_tmpl_t to a string writing the result to a buffer.
Definition: tmpl.c:1479
2nd highest priority debug messages (-xx | -X).
Definition: log.h:52
vp_tmpl_t * expiry_attr
Time at which the lease will expire.
VALUE_PAIR * config
VALUE_PAIR (s) used to set per request parameters for modules and the server core at runtime...
Definition: radiusd.h:227
#define IPPOOL_POOL_KEY
Definition: redis_ippool.h:60
int int map_to_request(REQUEST *request, vp_map_t const *map, radius_map_getvalue_t func, void *ctx)
Convert vp_map_t to VALUE_PAIR (s) and add them to a REQUEST.
Definition: map.c:1019
#define RDEBUG_ENABLED3
True if request debug level 1-3 messages are enabled.
Definition: log.h:239
Time value (struct timeval), only for config items.
Definition: radius.h:55
vp_tmpl_t * pool_name
Name of the pool we're allocating IP addresses from.
Metadata exported by the module.
Definition: modules.h:134
Configuration parameters for a redis connection.
Definition: redis.h:88
#define EOL
char const * name
Raw string used to create the template.
Definition: tmpl.h:190
vp_tmpl_t * lhs
Typically describes the attribute to add, modify or compare.
Definition: map.h:47
static CONF_PARSER redis_config[]
7 methods index for postauth section.
Definition: modules.h:48
FR_NAME_NUMBER const redis_reply_types[]
Definition: redis.c:29
Script doesn't exist.
Definition: redis.h:75
3rd highest priority debug messages (-xxx | -Xx).
Definition: log.h:53
uint8_t prefix
Prefix length - Between 0-32 for IPv4 and 0-128 for IPv6.
Definition: inet.h:47
vp_tmpl_t * gateway_id
Gateway identifier, usually NAS-Identifier or the actual Option 82 gateway.
Dictionary attribute.
Definition: tmpl.h:133
#define SHA1_DIGEST_LENGTH
Definition: sha1.h:18
vp_tmpl_t * rhs
Typically describes a literal value or a src attribute to copy or compare.
Definition: map.h:48
#define RLM_TYPE_THREAD_SAFE
Module is threadsafe.
Definition: modules.h:75
#define CC_HINT(_x)
Definition: build.h:71
static fr_redis_rcode_t ippool_script(redisReply **out, REQUEST *request, fr_redis_cluster_t *cluster, uint8_t const *key, size_t key_len, uint32_t wait_num, uint32_t wait_timeout, char const digest[], char const *script, char const *cmd,...)
Execute a script against Redis cluster.
#define RLM_MODULE_INIT
Definition: modules.h:86
VALUE_PAIR * vps
Result of decoding the packet into VALUE_PAIRs.
Definition: libradius.h:162
#define CONF_PARSER_TERMINATOR
Definition: conffile.h:289
char const * prefix
Logging prefix for errors in fr_redis_cluster_conn_create.
Definition: redis.h:89
static char lua_update_cmd[]
Lua script for updating leases.
#define IPPOOL_MAX_KEY_PREFIX_SIZE
Definition: redis_ippool.h:59
#define PW_STATUS_ALIVE
Definition: radius.h:193
#define _IPPOOL_RCODE_DEVICE_MISMATCH
Definition: redis_ippool.h:39
static ippool_rcode_t redis_ippool_allocate(rlm_redis_ippool_t *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, uint8_t const *device_id, size_t device_id_len, uint8_t const *gateway_id, size_t gateway_id_len, uint32_t expires)
Allocate a new IP address from a pool.
#define fr_redis_reply_free(_p)
Wrap freeReplyObject so we consistently check for NULL pointers.
Definition: redis.h:56
#define PW_STATUS_START
Definition: radius.h:191
#define inst
struct timeval wait_timeout
How long we wait for slaves to acknowledge writing.
static int mod_instantiate(CONF_SECTION *conf, void *instance)
The module considers the request invalid.
Definition: radiusd.h:93
A redis cluster.
Definition: cluster.c:254
#define _IPPOOL_RCODE_NOT_FOUND
Definition: redis_ippool.h:37
void fr_redis_reply_print(log_lvl_t lvl, redisReply *reply, REQUEST *request, int idx)
Print the response data in a useful treelike form.
Definition: redis.c:139
uint32_t wait_num
How many slaves we want to acknowledge allocations or updates.
static rlm_rcode_t mod_post_auth(void *instance, REQUEST *request) CC_HINT(nonnull)
#define PW_TYPE_SUBSECTION
Definition: conffile.h:188
Defines a CONF_PAIR to C data type mapping.
Definition: conffile.h:267
static ippool_rcode_t redis_ippool_update(rlm_redis_ippool_t *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, fr_ipaddr_t *ip, uint8_t const *device_id, size_t device_id_len, uint8_t const *gateway_id, size_t gateway_id_len, uint32_t expires)
Update an existing IP address in a pool.
void fr_sha1_init(fr_sha1_ctx *context)
Definition: sha1.c:94
struct rlm_redis_ippool rlm_redis_ippool_t
rlm_redis module instance
static int mod_bootstrap(CONF_SECTION *conf, void *instance)
#define is_truncated(_ret, _max)
Definition: libradius.h:204
int af
Address family.
Definition: inet.h:42
#define rad_assert(expr)
Definition: rad_assert.h:38
static char lua_update_digest[(SHA1_DIGEST_LENGTH *2)+1]
int fr_inet_pton(fr_ipaddr_t *out, char const *value, ssize_t inlen, int af, bool resolve, bool mask)
Simple wrapper to decide whether an IP value is v4 or v6 and call the appropriate parser...
Definition: inet.c:564
redisContext * handle
Hiredis context used when issuing commands.
Definition: redis.h:81
vp_tmpl_t * range_attr
Attribute to write the range ID to.
int value_data_cast(TALLOC_CTX *ctx, value_data_t *dst, PW_TYPE dst_type, fr_dict_attr_t const *dst_enumv, PW_TYPE src_type, fr_dict_attr_t const *src_enumv, value_data_t const *src)
Convert one type of value_data_t to another.
Definition: value.c:1073
Value in native format.
Definition: tmpl.h:138
static char lua_alloc_digest[(SHA1_DIGEST_LENGTH *2)+1]
module_t rlm_redis_ippool
#define REDIS_COMMON_CONFIG
Definition: redis.h:106
static char lua_alloc_cmd[]
Lua script for allocating new leases.
fr_redis_cluster_t * cluster
Redis cluster.
#define _IPPOOL_RCODE_SUCCESS
Definition: redis_ippool.h:36
CONF_SECTION * cf_subsection_find_next(CONF_SECTION const *section, CONF_SECTION const *subsection, char const *name1)
Definition: conffile.c:3799
void fr_redis_version_print(void)
Print the version of libhiredis the server was built against.
Definition: redis.c:52
#define STRINGIFY(x)
Definition: build.h:34
vp_tmpl_t * lease_time
How long an IP address should be allocated for.
union fr_ipaddr_t::@1 ipaddr
Unrecoverable library/server error.
Definition: redis.h:69
3 methods index for accounting section.
Definition: modules.h:44
static rlm_rcode_t mod_action(rlm_redis_ippool_t *inst, REQUEST *request, ippool_action_t action)
#define IPPOOL_SPRINT_IP(_buff, _ip, _prefix)
If the prefix is as wide as the AF data size then print it without CIDR notation. ...
Definition: redis_ippool.h:117
Stores an attribute, a value and various bits of other data.
Definition: pair.h:112
rlm_redis module instance
Redis connection sequence state.
Definition: cluster.h:43
void fr_sha1_final(uint8_t digest[20], fr_sha1_ctx *context)
Definition: sha1.c:132
A truth value.
Definition: radius.h:56
Definition: token.h:45
32 Bit unsigned integer.
Definition: radius.h:34
enum rlm_rcodes rlm_rcode_t
Return codes indicating the result of the module call.
tmpl_type_t type
What type of value tmpl refers to.
Definition: tmpl.h:188
static rs_t * conf
Definition: radsniff.c:46
char const * fr_strerror(void)
Get the last library error.
Definition: log.c:212
#define IPPOOL_DEVICE_KEY
Definition: redis_ippool.h:62
size_t len
Length of the raw string used to create the template.
Definition: tmpl.h:191
bool ipv4_integer
Whether IPv4 addresses should be cast to integers, for renew operations.
char const * cf_section_name1(CONF_SECTION const *cs)
Definition: conffile.c:3592
Module succeeded without doing anything.
Definition: radiusd.h:96
#define RDEBUG2(fmt,...)
Definition: log.h:244
unsigned int state
Definition: proto_bfd.c:200
fr_redis_rcode_t fr_redis_cluster_state_next(fr_redis_cluster_state_t *state, fr_redis_conn_t **conn, fr_redis_cluster_t *cluster, REQUEST *request, fr_redis_rcode_t status, redisReply **reply)
Get the next connection to attempt a command against.
Definition: cluster.c:1714
size_t length
Length of value data.
Definition: pair.h:87
uint64_t magic
Used to validate module struct.
Definition: modules.h:135
Module failed, don't reply.
Definition: radiusd.h:90
#define TAG_ANY
Definition: pair.h:191
enum log_lvl log_lvl_t
bool copy_on_update
Copy the address provided by ip_address to the reply_attr if updates are successful.
vp_tmpl_t * device_id
Unique device identifier.
#define FR_CONF_OFFSET(_n, _t, _s, _f)
Definition: conffile.h:168
static char lua_release_digest[(SHA1_DIGEST_LENGTH *2)+1]
ippool_rcode_t
Definition: redis_ippool.h:43
static CONF_PARSER module_config[]
Common functions for interacting with Redis via hiredis.
char * fr_asprint(TALLOC_CTX *ctx, char const *in, ssize_t inlen, char quote)
Escape string that may contain binary data, and write it to a new buffer.
Definition: print.c:390
Try the operation again.
Definition: redis.h:70
RADIUS_PACKET * packet
Incoming request.
Definition: radiusd.h:221
vp_tmpl_t * offer_time
How long we should reserve a lease for during the pre-allocation stage (typically responding to DHCP ...
static int ippool_wait_check(REQUEST *request, uint32_t wait_num, redisReply *reply)
Check the requisite number of slaves replicated the lease info.
#define PW_STATUS_ACCOUNTING_ON
Definition: radius.h:194
static ippool_rcode_t redis_ippool_release(rlm_redis_ippool_t *inst, REQUEST *request, uint8_t const *key_prefix, size_t key_prefix_len, fr_ipaddr_t *ip, uint8_t const *device_id, size_t device_id_len)
Release an existing IP address in a pool.
bool fr_redis_cluster_min_version(fr_redis_cluster_t *cluster, char const *min_version)
Check if members of the cluster are above a certain version.
Definition: cluster.c:2031
fr_redis_cluster_t * fr_redis_cluster_alloc(TALLOC_CTX *ctx, CONF_SECTION *module, fr_redis_conf_t *conf)
Allocate and initialise a new cluster structure.
Definition: cluster.c:2059
#define REDEBUG(fmt,...)
Definition: log.h:254
fr_redis_rcode_t fr_redis_pipeline_result(fr_redis_rcode_t *rcode, redisReply *out[], size_t out_len, fr_redis_conn_t *conn, int pipelined)
Simplifies handling of pipelined commands with Redis cluster.
Definition: redis.c:460
Common functions for interacting with Redis cluster via Hiredis.
fr_redis_rcode_t
Codes are ordered inversely by priority.
Definition: redis.h:67
#define _IPPOOL_RCODE_POOL_EMPTY
Definition: redis_ippool.h:40
int map_to_vp(TALLOC_CTX *ctx, VALUE_PAIR **out, REQUEST *request, vp_map_t const *map, void *uctx) CC_HINT(nonnull(2
#define PW_TYPE_REQUIRED
Error out if no matching CONF_PAIR is found, and no dflt value is set.
Definition: conffile.h:200
VALUE_PAIR * fr_pair_find_by_num(VALUE_PAIR *head, unsigned int vendor, unsigned int attr, int8_t tag)
Find the pair with the matching attribute.
Definition: pair.c:639
fr_redis_conf_t conf
Connection parameters for the Redis server.
#define PW_STATUS_STOP
Definition: radius.h:192
fr_redis_rcode_t fr_redis_cluster_state_init(fr_redis_cluster_state_t *state, fr_redis_conn_t **conn, fr_redis_cluster_t *cluster, REQUEST *request, uint8_t const *key, size_t key_len, bool read_only)
Resolve a key to a pool, and reserve a connection in that pool.
Definition: cluster.c:1594
static ssize_t ippool_pool_name(uint8_t out[], size_t outlen, rlm_redis_ippool_t *inst, REQUEST *request)
Find the pool name we'll be allocating from.
IPv4/6 prefix.
Definition: inet.h:41
char const * fr_int2str(FR_NAME_NUMBER const *table, int number, char const *def)
Definition: token.c:506
#define PW_TYPE_ATTRIBUTE
Value must resolve to attribute in dict (deprecated, use PW_TYPE_TMPL).
Definition: conffile.h:201
#define FR_TIMEVAL_TO_MS(_x)
Definition: conffile.h:235
#define fr_redis_pipeline_free(_r, _n)
Definition: redis.h:142
String of printable characters.
Definition: radius.h:33
#define FR_CONF_POINTER(_n, _t, _p)
Definition: conffile.h:172
Operation was successfull.
Definition: redis.h:68
#define RDEBUGX(_l, fmt,...)
Definition: log.h:242
#define RWDEBUG(fmt,...)
Definition: log.h:251
static rlm_rcode_t mod_accounting(void *instance, REQUEST *request) CC_HINT(nonnull)
#define PW_TYPE_TMPL
CONF_PAIR should be parsed as a template.
Definition: conffile.h:208
1 methods index for authorize section.
Definition: modules.h:42
User not found.
Definition: radiusd.h:95
ippool_action_t
Definition: redis_ippool.h:52
#define RCSID(id)
Definition: build.h:135
static void ippool_action_print(REQUEST *request, ippool_action_t action, log_lvl_t lvl, uint8_t const *key_prefix, size_t key_prefix_len, char const *ip_str, uint8_t const *device_id, size_t device_id_len, uint8_t const *gateway_id, size_t gateway_id_len, uint32_t expires)
OK (pairs modified).
Definition: radiusd.h:97
32 Bit IPv4 Address.
Definition: radius.h:35
Value pair map.
Definition: map.h:46
static char lua_release_cmd[]
Lua script for releasing leases.
#define FR_IPADDR_PREFIX_STRLEN
Like FR_IPADDR_STRLEN but with space for a prefix.
Definition: inet.h:71
#define PW_STATUS_ACCOUNTING_OFF
Definition: radius.h:195
A source or sink of value data.
Definition: tmpl.h:187
vp_tmpl_t * reply_attr
IP attribute and destination.
size_t fr_bin2hex(char *hex, uint8_t const *bin, size_t inlen)
Convert binary data to a hex string.
Definition: misc.c:254
#define ERROR(fmt,...)
Definition: log.h:145
#define IPPOOL_ADDRESS_KEY
Definition: redis_ippool.h:61
char const * name
Instance name.
vp_tmpl_t * ip_address
Attribute to read the IP for renewal from.
char const * cf_section_name2(CONF_SECTION const *cs)
Definition: conffile.c:3601
Connection handle, holding a redis context.
Definition: redis.h:80
#define RDEBUG3(fmt,...)
Definition: log.h:245