Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rlm_redis_ippool: break out lua scripts as defacto schema (take 2) #3467

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ env:
- SQL_POSTGRESQL_TEST_SERVER="127.0.0.1"
- LDAP_TEST_SERVER="127.0.0.1"
- LDAP_TEST_SERVER_PORT="3890"
# - REDIS_TEST_SERVER="127.0.0.1"
- REDIS_TEST_SERVER="127.0.0.1"
- REDIS_IPPOOL_TEST_SERVER="127.0.0.1"
- ANALYZE_C_DUMP="1"
- FR_GLOBAL_POOL=4M
Expand Down
31 changes: 26 additions & 5 deletions doc/antora/modules/raddb/pages/mods-available/redis_ippool.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@




= Redis IP Pool Module

The `redis_ippool` module implements a fast and scalable IP
Expand All @@ -15,7 +11,7 @@ the Redis cluster.



## Configuration Settings
== Configuration Settings

Al configuration items at this level (below the `redis` block)
are polymorphic, meaning `xlats`, attribute references, literal values
Expand Down Expand Up @@ -144,3 +140,28 @@ redis_ippool {
}
}
```

== Schema

Described is the use of a Redis KV store for IP pool management. Its use becomes appropriate only when load reaches a point where decoupling the live 'current' pool usage state with the historical usage is necessary. Understanding this is relevent only if you wish to change the contents of `raddb/mods-config/redis/ippool/`.

Keys in Redis are of the form `link:https://redis.io/topics/cluster-tutorial#redis-cluster-data-sharding[{POOLNAME}:TYPE:VALUE` where `TYPE` matches the namespaces:

pool (`pool`) [sorted set]:: IP addresses are added to the pool and the score is an link:https://en.wikipedia.org/wiki/Unix_time#Encoding_time_as_a_number[epoch time in seconds] that represents when the lease _expires_
address (`ip`) [hash]:: stores value options about the IP address (`range_id`, `device` - current/last client identifier, ...); one instance of this key exists for every pool address entry
device (`device`) [key]:: contains IP the device was last set to; this key expires 10x the lease time to support sticky IPs

Note `device` has an link:https://redis.io/commands/expire[expiry time] of 10x the lease time to enable sticky IP addresses on reconnects (if the lease is still free) as well optionally providing a a recent use log datastore.

An example of the key layout is (where the poolname is `local`):

[source,shell]
----
localhost:30001> KEYS *
1) "{local}:ip:192.0.2.0"
2) "{local}:ip:192.0.2.1"
3) "{local}:ip:192.0.2.100"
4) "{local}:ip:192.0.2.10"
5) "{local}:device:00:11:22:33:44:55"
6) "{local}:pool"
----
16 changes: 16 additions & 0 deletions raddb/mods-available/redis_ippool
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ redis_ippool {
#
copy_on_update = yes

#
# paths to Lua scripts that are uploaded and interacted with in Redis; to creates a
# kind of defacto schema in which how data is laid out
#
# `lua_preamble` is special in that is is prepended to all the other scripts to
# provide library functions and constant definitions.
#
#lua_preamble = ${modconfdir}/redis_ippool/preamble.lua
#lua_add = ${modconfdir}/redis_ippool/add.lua
#lua_alloc = ${modconfdir}/redis_ippool/alloc.lua
#lua_delete = ${modconfdir}/redis_ippool/delete.lua
#lua_release = ${modconfdir}/redis_ippool/release.lua
#lua_show = ${modconfdir}/redis_ippool/show.lua
#lua_stats = ${modconfdir}/redis_ippool/stats.lua
#lua_update = ${modconfdir}/redis_ippool/update.lua

#
# redis { ... }:: Redis connection settings.
#
Expand Down
69 changes: 69 additions & 0 deletions raddb/mods-config/lua/example.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
function tprint (tbl, indent)
if not indent then indent = 0 end

for k, v in tbl.pairs() do
formatting = string.rep(" ", indent) .. k .. ": "
if type(v) == "table" then
print(formatting)
tprint(v, indent+1)
else
print(formatting .. '"' .. v .. '" (' .. type(v) .. ')')
end
end
end

function preacct()
print("example.lua/preacct()")
return fr.ok
end

function accounting()
print("example.lua/accounting()")
return fr.ok
end

function post_auth()
print("example.lua/post_auth()")
return fr.ok
end

function instantiate()
print("example.lua/instantiate()")
return fr.ok
end

function detach()
print("example.lua/detach()")
return fr.ok
end

function xlat()
print("example.lua/xlat()")
return fr.ok
end

function authenticate()
print("example.lua/authenticate()")
return fr.ok
end

function authorize()
-------------------------
-- example invocations --
-------------------------

--tprint(get_attribute("user-name"))
--tprint(get_attribute("user-password"))
--tprint(get_attribute("tunnel-type", "2"))
--print(request['user-name'][0])
--print(request['user-name'].next_iter())
--print(request['user-name'].next_iter())
--tprint(request['user-name'])
--tprint(request['user-name'])

print("example.lua/authorize()")
print("Request list contents:")
tprint(fr.request, 2)

return fr.ok
end
48 changes: 48 additions & 0 deletions raddb/mods-config/redis_ippool/add.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
-- Lua script for adding a pool
--
-- - KEYS[1] The pool name.
-- - ARGV[1] IP address/range of lease(s).
-- - ARGV[2] Prefix to add (0 = auto => 64 for IPv6, 32 for IPv4).
-- - ARGV[3] (optional) Range ID.
--
-- Returns array { <rcode>[, <counter> ] }
-- - IPPOOL_RCODE_SUCCESS
-- - IPPOOL_RCODE_FAIL

local ok
local range
local prefix

local pool_key

local counter

ok, range = pcall(iptool.parse, ARGV[1])
if not ok then
return { ippool_rcode_fail }
end
prefix = toprefix(ARGV[1], ARGV[2])
if guard(range, prefix) then
return { ippool_rcode_fail }
end

pool_key = "{" .. KEYS[1] .. "}:" .. ippool_key.pool

counter = 0
for addr in iptool.iter(range, prefix) do
local ret = redis.call("ZADD", pool_key, "NX", 0, addr)
local address_key = "{" .. KEYS[1] .. "}:" .. ippool_key.address .. ":" .. addr

if ARGV[3] then
ret = redis.call("HSET", address_key, "range", ARGV[3]) or ret
else
ret = redis.call("HDEL", address_key, "range") or ret
end

counter = counter + ret
end

return {
ippool_rcode.success,
counter
}
78 changes: 78 additions & 0 deletions raddb/mods-config/redis_ippool/alloc.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
-- Lua script for allocating new leases
--
-- - KEYS[1] The pool name.
-- - ARGV[1] Expires in (seconds).
-- - ARGV[2] Device identifier (administratively configured).
-- - ARGV[3] (optional) Gateway identifier.
--
-- Sticky IPs work by setting the TTL on the device_key to 10x ARGV[1]
--
-- Returns { <rcode>[, <ip>, <range>, <lease time> ] }
-- - IPPOOL_RCODE_SUCCESS lease updated.
-- - IPPOOL_RCODE_POOL_EMPTY no available leases in pool.

local pool_key
local address_key
local device_key

local time
local expires_in

local ip

pool_key = "{" .. KEYS[1] .. "}:" .. ippool_key.pool
device_key = "{" .. KEYS[1] .. "}:" .. ippool_key.device .. ":" .. ARGV[2]

time = tonumber(redis.call("TIME")[1])
expires_in = tonumber(ARGV[1])

-- Check to see if the client already has a lease,
-- and if it does return that.
--
-- The additional sanity checks are to allow for the record
-- of device/ip binding to persist for longer than the lease.
ip = redis.call("GET", device_key)
if ip then
local epoch = tonumber(redis.call("ZSCORE", pool_key, ip))
if epoch == nil then
ip = nil
-- when positive, we have to check it is still for the same device
elseif epoch - time >= 0 then
address_key = "{" .. KEYS[1] .. "}:" .. ippool_key.address .. ":" .. ip
if redis.call("HGET", address_key, "device") ~= ARGV[2] then
ip = nil
end
end
end

-- ...else, get the IP address which expired the longest time ago.
if not ip then
ip = redis.call("ZREVRANGE", pool_key, -1, -1, "WITHSCORES")
if not ip or #ip < 2 or tonumber(ip[2]) >= time then
return { ippool_rcode.pool_empty }
end
ip = ip[1]
end

address_key = "{" .. KEYS[1] .. "}:" .. ippool_key.address .. ":" .. ip

redis.call("ZADD", pool_key, "XX", time + expires_in, ip)

redis.call("HSET", address_key, "device", ARGV[2])

if ARGV[3] then
redis.call("HSET", address_key, "gateway", ARGV[3])
else
redis.call("HDEL", address_key, "gateway")
end

redis.call("SET", device_key, ip, "EX", 10 * expires_in)

redis.call("HINCRBY", address_key, "counter", 1)

return {
ippool_rcode.success,
ip,
redis.call("HGET", address_key, "range"),
expires_in
}
56 changes: 56 additions & 0 deletions raddb/mods-config/redis_ippool/delete.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- Lua script for deleting a lease
--
-- - KEYS[1] The pool name.
-- - ARGV[1] IP address/range of lease(s).
-- - ARGV[2] Prefix to add (0 = auto => 64 for IPv6, 32 for IPv4).
--
-- Removes the IP entry in the ZSET, then removes the address hash, and the device key
-- if one exists.
--
-- Will work with partially removed IP addresses (where the ZSET entry is absent but other
-- elements were not cleaned up).
--
-- Returns array { <rcode>[, <counter> ] }
-- - IPPOOL_RCODE_SUCCESS
-- - IPPOOL_RCODE_FAIL

local ok
local range
local prefix

local pool_key

local counter

ok, range = pcall(iptool.parse, ARGV[1])
if not ok then
return { ippool_rcode_fail }
end
prefix = toprefix(ARGV[1], ARGV[2])
if guard(range, prefix) then
return { ippool_rcode_fail }
end

pool_key = "{" .. KEYS[1] .. "}:" .. ippool_key.pool

counter = 0
for addr in iptool.iter(range, prefix) do
local ret = redis.call("ZREM", pool_key, addr)
local address_key = "{" .. KEYS[1] .. "}:" .. ippool_key.address .. ":" .. addr

local found = redis.call("HGET", address_key, "device")
if found then
local device_key = "{" .. KEYS[1] .. "}:" .. ippool_key.device .. ":" .. found

ret = redis.call("DEL", address_key) or ret
-- Remove the association between the device and a lease
ret = redis.call("DEL", device_key) or ret
end

counter = counter + ret
end

return {
ippool_rcode.success,
counter
}