Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 2763 additions and 1707 deletions
......@@ -6,8 +6,8 @@
* (c) 2006 Robert Spalek <robert@ucw.cz>
* (c) 2007 Pavel Charvat <pchar@ucw.cz>
*
* This software may be freely distributed and used according to the terms
* of the GNU Lesser General Public License.
* SPDX-License-Identifier: LGPL-2.1-or-later
* Source: https://www.ucw.cz/libucw/
*/
#ifndef _UCW_LIB_H
......@@ -15,6 +15,7 @@
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#ifdef CONFIG_UCW_CLEAN_ABI
#define assert_failed ucw_assert_failed
......@@ -87,13 +88,7 @@
#ifdef __GNUC__
#define NONRET __attribute__((noreturn)) /** Function does not return **/
#define UNUSED __attribute__((unused)) /** Variable/parameter is knowingly unused **/
#define CONSTRUCTOR __attribute__((constructor)) /** Call function upon start of program **/
#define CONSTRUCTOR_WITH_PRIORITY(p) __attribute__((constructor(p))) /** Define constructor with a given priority **/
#define PACKED __attribute__((packed)) /** Structure should be packed **/
#define CONST __attribute__((const)) /** Function depends only on arguments **/
#define PURE __attribute__((pure)) /** Function depends only on arguments and global vars **/
#include "ccan/compiler/compiler.h"
#define FORMAT_CHECK(x,y,z) __attribute__((format(x,y,z))) /** Checking of printf-like format strings **/
#define likely(x) __builtin_expect((x),1) /** Use `if (likely(@x))` if @x is almost always true **/
#define unlikely(x) __builtin_expect((x),0) /** Use `if (unlikely(@x))` to hint that @x is almost always false **/
......@@ -123,7 +118,7 @@
* === Basic logging functions (see <<log:,Logging>> and <ucw/log.h> for more)
***/
#define DBG(x,y...) do { } while(0)
#define DBG(x, ...) do { } while(0)
#define DBG_SPOT do { } while(0)
#define ASSERT(x)
......
SPDXVersion: SPDX-2.1
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: libucw
DocumentNamespace: http://spdx.org/spdxdocs/spdx-v2.1-c3d39e26-6b71-46d4-88ea-e52750932ff3
PackageName: libucw
PackageDownloadLocation: git://git.ucw.cz/libucw.git@f1bde7104b04d5254d1d1d7dcc8de790a43a416f#ucw/
PackageOriginator: Organization: United Computer Wizards
PackageLicenseDeclared: LGPL-2.1-or-later
/*
* UCW Library -- Memory Pools (Formatting)
*
* (c) 2005 Martin Mares <mj@ucw.cz>
* (c) 2007 Pavel Charvat <pchar@ucw.cz>
* SPDX-License-Identifier: LGPL-2.1-or-later
* Source: https://www.ucw.cz/libucw/
*/
#include <ucw/lib.h>
#include <ucw/mempool.h>
#include <stdio.h>
#include <string.h>
/* FIXME: migrate to Knot DNS version of mempools. */
#pragma GCC diagnostic ignored "-Wpointer-arith"
static char *
mp_vprintf_at(struct mempool *mp, size_t ofs, const char *fmt, va_list args)
{
char *ret = mp_grow(mp, ofs + 1) + ofs;
va_list args2;
va_copy(args2, args);
int cnt = vsnprintf(ret, mp_avail(mp) - ofs, fmt, args2);
va_end(args2);
if (cnt < 0)
{
/* Our C library doesn't support C99 return value of vsnprintf, so we need to iterate */
do
{
ret = mp_expand(mp) + ofs;
va_copy(args2, args);
cnt = vsnprintf(ret, mp_avail(mp) - ofs, fmt, args2);
va_end(args2);
}
while (cnt < 0);
}
else if ((uint)cnt >= mp_avail(mp) - ofs)
{
ret = mp_grow(mp, ofs + cnt + 1) + ofs;
va_copy(args2, args);
vsnprintf(ret, cnt + 1, fmt, args2);
va_end(args2);
}
mp_end(mp, ret + cnt + 1);
return ret - ofs;
}
char *
mp_vprintf(struct mempool *mp, const char *fmt, va_list args)
{
mp_start(mp, 1);
return mp_vprintf_at(mp, 0, fmt, args);
}
char *
mp_printf(struct mempool *p, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
char *res = mp_vprintf(p, fmt, args);
va_end(args);
return res;
}
char *
mp_vprintf_append(struct mempool *mp, char *ptr, const char *fmt, va_list args)
{
size_t ofs = mp_open(mp, ptr);
ASSERT(ofs && !ptr[ofs - 1]);
return mp_vprintf_at(mp, ofs - 1, fmt, args);
}
char *
mp_printf_append(struct mempool *mp, char *ptr, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
char *res = mp_vprintf_append(mp, ptr, fmt, args);
va_end(args);
return res;
}
#ifdef TEST
int main(void)
{
struct mempool *mp = mp_new(64);
char *x = mp_printf(mp, "<Hello, %s!>", "World");
fputs(x, stdout);
x = mp_printf_append(mp, x, "<Appended>");
fputs(x, stdout);
x = mp_printf(mp, "<Hello, %50s!>\n", "World");
fputs(x, stdout);
return 0;
}
#endif
......@@ -4,8 +4,8 @@
* (c) 1997--2014 Martin Mares <mj@ucw.cz>
* (c) 2007--2015 Pavel Charvat <pchar@ucw.cz>
*
* This software may be freely distributed and used according to the terms
* of the GNU Lesser General Public License.
* SPDX-License-Identifier: LGPL-2.1-or-later
* Source: https://www.ucw.cz/libucw/
*/
#undef LOCAL_DEBUG
......@@ -18,6 +18,9 @@
#include <string.h>
#include <stdlib.h>
/* FIXME: migrate to Knot DNS version of mempools. */
#pragma GCC diagnostic ignored "-Wpointer-arith"
#define MP_CHUNK_TAIL ALIGN_TO(sizeof(struct mempool_chunk), CPU_STRUCT_ALIGN)
#define MP_SIZE_MAX (SIZE_MAX - MP_CHUNK_TAIL - CPU_PAGE_SIZE)
......@@ -86,9 +89,10 @@ static void *
mp_new_big_chunk(struct mempool *pool, size_t size)
{
struct mempool_chunk *chunk;
chunk = malloc(size + MP_CHUNK_TAIL) + size;
chunk = malloc(size + MP_CHUNK_TAIL);
if (!chunk)
return NULL;
chunk = (struct mempool_chunk *)((char *)chunk + size);
chunk->size = size;
if (pool)
pool->total_size += size + MP_CHUNK_TAIL;
......
......@@ -3,14 +3,14 @@
*
* (c) 1997--2015 Martin Mares <mj@ucw.cz>
* (c) 2007 Pavel Charvat <pchar@ucw.cz>
*
* This software may be freely distributed and used according to the terms
* of the GNU Lesser General Public License.
* SPDX-License-Identifier: LGPL-2.1-or-later
* Source: https://www.ucw.cz/libucw/
*/
#ifndef _UCW_POOLS_H
#define _UCW_POOLS_H
#include "lib/defines.h"
#include <ucw/alloc.h>
#include <ucw/config.h>
#include <ucw/lib.h>
......@@ -100,6 +100,7 @@ struct mempool_stats { /** Mempool statistics. See @mp_stats(). **/
*
* Memory pools can be treated as <<trans:respools,resources>>, see <<trans:res_mempool()>>.
**/
KR_EXPORT
void mp_init(struct mempool *pool, size_t chunk_size);
/**
......@@ -110,6 +111,7 @@ void mp_init(struct mempool *pool, size_t chunk_size);
*
* Memory pools can be treated as <<trans:respools,resources>>, see <<trans:res_mempool()>>.
**/
KR_EXPORT
struct mempool *mp_new(size_t chunk_size);
/**
......@@ -117,6 +119,7 @@ struct mempool *mp_new(size_t chunk_size);
* Frees all the memory allocated by this mempool and,
* if created by @mp_new(), the @pool itself.
**/
KR_EXPORT
void mp_delete(struct mempool *pool);
/**
......@@ -125,6 +128,7 @@ void mp_delete(struct mempool *pool);
* further allocation requests. Leaves the @pool alive,
* even if it was created with @mp_new().
**/
KR_EXPORT
void mp_flush(struct mempool *pool);
/**
......@@ -166,6 +170,7 @@ void *mp_alloc_internal(struct mempool *pool, size_t size) LIKE_MALLOC;
* `CPU_STRUCT_ALIGN` bytes and this condition remains true also
* after future reallocations.
**/
KR_EXPORT
void *mp_alloc(struct mempool *pool, size_t size);
/**
......@@ -225,7 +230,7 @@ static inline struct ucw_allocator *mp_get_allocator(struct mempool *mp)
* you can grow it incrementally to needed size. You can grow only
* one buffer at a time on a given mempool.
*
* Similar functionality is provided by <<growbuf:,growing buffes>> module.
* Similar functionality is provided by <<growbuf:,growing buffers>> module.
***/
/* For internal use only, do not call directly */
......@@ -531,6 +536,7 @@ char *mp_str_from_mem(struct mempool *p, const void *mem, size_t len) LIKE_MALLO
/**
* printf() into a in-memory string, allocated on the memory pool.
**/
KR_EXPORT
char *mp_printf(struct mempool *mp, const char *fmt, ...) FORMAT_CHECK(printf,2,3) LIKE_MALLOC;
/**
* Like @mp_printf(), but uses `va_list` for parameters.
......@@ -549,6 +555,7 @@ char *mp_vprintf(struct mempool *mp, const char *fmt, va_list args) LIKE_MALLOC;
* not called on an opened growing buffer. The old name will be preserved for backward
* compatibility for the time being.
**/
KR_EXPORT
char *mp_printf_append(struct mempool *mp, char *ptr, const char *fmt, ...) FORMAT_CHECK(printf,3,4);
#define mp_append_printf mp_printf_append
/**
......
************************
Knot DNS Resolver daemon
************************
Requirements
============
* libuv_ 1.0+ (a multi-platform support library with a focus on asynchronous I/O)
* Lua_ 5.1+ (embeddable scripting language, LuaJIT_ is preferred)
Running
=======
There is a separate resolver library in the `lib` directory, and a minimalistic daemon in
the `daemon` directory.
.. code-block:: bash
$ ./daemon/kresd -h
Interacting with the daemon
---------------------------
The daemon features a CLI interface if launched interactively, type ``help`` to see the list of available commands.
You can load modules this way and use their properties to get information about statistics and such.
.. code-block:: bash
$ kresd /var/run/knot-resolver
[system] started in interactive mode, type 'help()'
> cache.count()
53
.. role:: lua(code)
:language: lua
Running in forked mode
----------------------
The server can clone itself into multiple processes upon startup, this enables you to scale it on multiple cores.
.. code-block:: bash
$ kresd -f 2 rundir > kresd.log
.. note:: On recent Linux supporting ``SO_REUSEPORT`` (since 3.9, backported to RHEL 2.6.32) it is also able to bind to the same endpoint and distribute the load between the forked processes. If the kernel doesn't support it, you can still fork multiple processes on different ports, and do load balancing externally (on firewall or with `dnsdist <http://dnsdist.org/>`_).
Notice it isn't interactive, but you can attach to the the consoles for each process, they are in ``rundir/tty/PID``.
.. code-block:: bash
$ nc -U rundir/tty/3008 # or socat - UNIX-CONNECT:rundir/tty/3008
> cache.count()
53
This is also a way to enumerate and test running instances, the list of files int ``tty`` correspond to list
of running processes, and you can test the process for liveliness by connecting to the UNIX socket.
.. warning:: This is very basic way to orchestrate multi-core deployments and doesn't scale in multi-node clusters. Keep an eye on the prepared ``hive`` module that is going to automate everything from service discovery to deployment and consistent configuration.
Configuration
=============
.. contents::
:depth: 2
:local:
In it's simplest form it requires just a working directory in which it can set up persistent files like
cache and the process state. If you don't provide the working directory by parameter, it is going to make itself
comfortable in the current working directory.
.. code-block:: sh
$ kresd /var/run/kresd
And you're good to go for most use cases! If you want to use modules or configure daemon behavior, read on.
There are several choices on how you can configure the daemon, a RPC interface a CLI and a configuration file.
Fortunately all share common syntax and are transparent to each other, e.g. changes made during the runtime are kept
in the redo log and are immediately visible.
.. warning:: Redo log is not yet implemented, changes are visible during the process lifetime only.
Configuration example
---------------------
.. code-block:: lua
-- 10MB cache
cache.size = 10*MB
-- load some modules
modules = { 'hints', 'cachectl' }
-- interfaces
net = { '127.0.0.1' }
Configuration syntax
--------------------
The configuration is kept in the ``config`` file in the daemon working directory, and it's going to get loaded automatically.
If there isn't one, the daemon is going to start with sane defaults, listening on `localhost`.
The syntax for options is like follows: ``group.option = value`` or ``group.action(parameters)``.
You can also comment using a ``--`` prefix.
A simple example would be to load static hints.
.. code-block:: lua
modules = {
'hints' -- no configuration
}
If the module accepts accepts configuration, you can call the ``module.config({...})`` or provide options table.
The syntax for table is ``{ key1 = value, key2 = value }``, and it represents the unpacked `JSON-encoded`_ string, that
the modules use as the :ref:`input configuration <mod-properties>`.
.. code-block:: lua
modules = {
cachectl = true,
hints = { -- with configuration
file = '/etc/hosts'
}
}
The possible simple data types are: string, integer or float, and boolean.
.. tip:: The configuration and CLI syntax is Lua language, with which you may already be familiar with.
If not, you can read the `Learn Lua in 15 minutes`_ for a syntax overview. Spending just a few minutes
will allow you to break from static configuration, write more efficient configuration with iteration, and
leverage events and hooks. Lua is heavily used for scripting in applications ranging from embedded to game engines,
but in DNS world notably in `PowerDNS Recursor`_. Knot DNS Resolver does not simply use Lua modules, but it is
the heart of the daemon for everything from configuration, internal events and user interaction.
Dynamic configuration
^^^^^^^^^^^^^^^^^^^^^
Knowing that the the configuration is a Lua in disguise enables you to write dynamic rules, and also avoid
repetition and templating. This is unavoidable with static configuration, e.g. when you want to configure
each node a little bit differently.
.. code-block:: lua
if hostname() == 'hidden' then
net.listen(net.eth0, 5353)
else
net = { '127.0.0.1', net.eth1.addr[1] }
end
Another example would show how it is possible to bind to all interfaces, using iteration.
.. code-block:: lua
for name, addr_list in pairs(net.interfaces()) do
net.listen(addr_list)
end
You can also use third-party packages (available for example through LuaRocks_) as on this example
to download cache from parent, to avoid cold-cache start.
.. code-block:: lua
local http = require('socket.http')
local ltn12 = require('ltn12')
if cache.count() == 0 then
-- download cache from parent
http.request {
url = 'http://parent/cache.mdb',
sink = ltn12.sink.file(io.open('cache.mdb', 'w'))
}
-- reopen cache with 100M limit
cache.open(100*MB)
end
Events and services
^^^^^^^^^^^^^^^^^^^
The Lua supports a concept called closures_, this is extremely useful for scripting actions upon various events,
say for example - prune the cache within minute after loading, publish statistics each 5 minutes and so on.
Here's an example of an anonymous function with :func:`event.recurrent()`:
.. code-block:: lua
-- every 5 minutes
event.recurrent(5 * minute, function()
cachectl.prune()
end)
Note that each scheduled event is identified by a number valid for the duration of the event,
you may cancel it at any time. You can do this with anonymous functions, if you accept the event
as a parameter, but it's not very useful as you don't have any *non-global* way to keep persistent variables.
.. code-block:: lua
-- make a closure, encapsulating counter
function pruner()
local i = 0
-- pruning function
return function(e)
cachectl.prune()
-- cancel event on 5th attempt
i = i + 1
if i == 5 then
event.cancel(e)
fi
end
end
-- make recurrent event that will cancel after 5 times
event.recurrent(5 * minute, pruner())
* File watchers
* Data I/O
.. note:: Work in progress, come back later!
.. _closures: http://www.lua.org/pil/6.1.html
Configuration reference
-----------------------
This is a reference for variables and functions available to both configuration file and CLI.
.. contents::
:depth: 1
:local:
Environment
^^^^^^^^^^^
.. envvar:: env (table)
Return environment variable.
.. code-block:: lua
env.USER -- equivalent to $USER in shell
.. function:: hostname()
:return: Machine hostname.
Network configuration
^^^^^^^^^^^^^^^^^^^^^
For when listening on ``localhost`` just doesn't cut it.
.. tip:: Use declarative interface for network.
.. code-block:: lua
net = { '127.0.0.1', net.eth0, net.eth1.addr[1] }
.. function:: net.listen(address, [port = 53])
:return: boolean
Listen on address, port is optional.
.. function:: net.listen({address1, ...}, [port = 53])
:return: boolean
Listen on list of addresses.
.. function:: net.listen(interface, [port = 53])
:return: boolean
Listen on all addresses belonging to an interface.
Example:
.. code-block:: lua
net.listen(net.eth0) -- listen on eth0
.. function:: net.close(address, [port = 53])
:return: boolean
Close opened address/port pair, noop if not listening.
.. function:: net.list()
:return: Table of bound interfaces.
Example output:
.. code-block:: lua
[127.0.0.1] => {
[port] => 53
[tcp] => true
[udp] => true
}
.. function:: net.interfaces()
:return: Table of available interfaces and their addresses.
Example output:
.. code-block:: lua
[lo0] => {
[addr] => {
[1] => ::1
[2] => 127.0.0.1
}
[mac] => 00:00:00:00:00:00
}
[eth0] => {
[addr] => {
[1] => 192.168.0.1
}
[mac] => de:ad:be:ef:aa:bb
}
.. tip:: You can use ``net.<iface>`` as a shortcut for specific interface, e.g. ``net.eth0``
Modules configuration
^^^^^^^^^^^^^^^^^^^^^
The daemon provides an interface for dynamic loading of :ref:`daemon modules <modules-implemented>`.
.. tip:: Use declarative interface for module loading.
.. code-block:: lua
modules = { 'cachectl' }
modules = {
hints = {file = '/etc/hosts'}
}
Equals to:
.. code-block:: lua
modules.load('cachectl')
modules.load('hints')
hints.config({file = '/etc/hosts'})
.. function:: modules.list()
:return: List of loaded modules.
.. function:: modules.load(name)
:param string name: Module name, e.g. "hints"
:return: boolean
Load a module by name.
.. function:: modules.unload(name)
:param string name: Module name
:return: boolean
Unload a module by name.
Cache configuration
^^^^^^^^^^^^^^^^^^^
The cache in Knot DNS Resolver is persistent with LMDB backend, this means that the daemon doesn't lose
the cached data on restart or crash to avoid cold-starts. The cache may be reused between cache
daemons or manipulated from other processes, making for example synchronised load-balanced recursors possible.
.. envvar:: cache.size (number)
Get/set the cache maximum size in bytes. Note that this is only a hint to the backend,
which may or may not respect it. See :func:`cache.open()`.
.. code-block:: lua
print(cache.size)
cache.size = 100 * MB -- equivalent to `cache.open(100 * MB)`
.. envvar:: cache.storage (string)
Get or change the cache storage backend configuration, see :func:`cache.backends()` for
more information. If the new storage configuration is invalid, it is not set.
.. code-block:: lua
print(cache.storage)
cache.storage = 'lmdb://.'
.. function:: cache.backends()
:return: map of backends
The cache supports runtime-changeable backends, using the optional :rfc:`3986` URI, where the scheme
represents backend protocol and the rest of the URI backend-specific configuration. By default, it
is a ``lmdb`` backend in working directory, i.e. ``lmdb://``.
Example output:
.. code-block:: lua
[lmdb://] => true
.. function:: cache.stats()
:return: table of cache counters
The cache collects counters on various operations (hits, misses, transactions, ...). This function call returns a table of
cache counters that can be used for calculating statistics.
.. function:: cache.open(max_size[, config_uri])
:param number max_size: Maximum cache size in bytes.
:return: boolean
Open cache with size limit. The cache will be reopened if already open.
Note that the max_size cannot be lowered, only increased due to how cache is implemented.
.. tip:: Use ``kB, MB, GB`` constants as a multiplier, e.g. ``100*MB``.
The cache supports runtime-changeable backends, see :func:`cache.backends()` for mor information and
default. Refer to specific documentation of specific backends for configuration string syntax.
- ``lmdb://``
As of now it only allows you to change the cache directory, e.g. ``lmdb:///tmp/cachedir``.
.. function:: cache.count()
:return: Number of entries in the cache.
.. function:: cache.close()
:return: boolean
Close the cache.
.. note:: This may or may not clear the cache, depending on the used backend. See :func:`cachectl.clear()`.
.. function:: cache.stats()
Return table of statistics, note that this tracks all operations over cache, not just which
queries were answered from cache or not.
Example:
.. code-block:: lua
print('Insertions:', cache.stats().insert)
Timers and events
^^^^^^^^^^^^^^^^^
The timer represents exactly the thing described in the examples - it allows you to execute closures
after specified time, or event recurrent events. Time is always described in milliseconds,
but there are convenient variables that you can use - ``sec, minute, hour``.
For example, ``5 * hour`` represents five hours, or 5*60*60*100 milliseconds.
.. function:: event.after(time, function)
:return: event id
Execute function after the specified time has passed.
The first parameter of the callback is the event itself.
Example:
.. code-block:: lua
event.after(1 * minute, function() print('Hi!') end)
.. function:: event.recurrent(interval, function)
:return: event id
Similar to :func:`event.after()`, periodically execute function after ``interval`` passes.
Example:
.. code-block:: lua
msg_count = 0
event.recurrent(5 * sec, function(e)
msg_count = msg_count + 1
print('Hi #'..msg_count)
end)
.. function:: event.cancel(event_id)
Cancel running event, it has no effect on already canceled events.
New events may reuse the event_id, so the behaviour is undefined if the function
is called after another event is started.
Example:
.. code-block:: lua
e = event.after(1 * minute, function() print('Hi!') end)
event.cancel(e)
Scripting worker
^^^^^^^^^^^^^^^^
Worker is a service over event loop that tracks and schedules outstanding queries,
you can see the statistics or schedule new queries.
.. function:: worker.stats()
Return table of statistics.
* ``udp`` - number of outbound queries over UDP
* ``tcp`` - number of outbound queries over TCP
* ``ipv6`` - number of outbound queries over IPv6
* ``ipv4`` - number of outbound queries over IPv4
* ``concurrent`` - number of concurrent queries at the moment
Example:
.. code-block:: lua
print(worker.stats().concurrent)
.. function:: worker.resolve(qname, qtype[, qclass = kres.class.IN, options = 0])
:param string qname: Query name (e.g. 'com.')
:param number qtype: Query type (e.g. ``kres.type.NS``)
:param number qclass: Query class *(optional)* (e.g. ``kres.class.IN``)
:param number options: Resolution options (see query flags)
:return: boolean
Resolve a query, there is currently no callback when its finished, but you can track the query
progress in layers, just like any other query.
.. _`JSON-encoded`: http://json.org/example
.. _`Learn Lua in 15 minutes`: http://tylerneylon.com/a/learn-lua/
.. _`PowerDNS Recursor`: https://doc.powerdns.com/md/recursor/scripting/
.. _LuaRocks: https://rocks.moonscript.org/
.. _libuv: https://github.com/libuv/libuv
.. _Lua: http://www.lua.org/about.html
.. _LuaJIT: http://luajit.org/luajit.html
\ No newline at end of file
/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <uv.h>
#include <libknot/descriptor.h>
#include "lib/cache.h"
#include "daemon/bindings.h"
#include "daemon/worker.h"
/** @internal Prefix error with file:line */
static int format_error(lua_State* L, const char *err)
{
lua_Debug d;
lua_getstack(L, 1, &d);
/* error message prefix */
lua_getinfo(L, "Sln", &d);
if (strncmp(d.short_src, "[", 1) != 0) {
lua_pushstring(L, d.short_src);
lua_pushstring(L, ":");
lua_pushnumber(L, d.currentline);
lua_pushstring(L, ": error: ");
lua_concat(L, 4);
} else {
lua_pushstring(L, "error: ");
}
/* error message */
lua_pushstring(L, err);
lua_concat(L, 2);
return 1;
}
/** List loaded modules */
static int mod_list(lua_State *L)
{
struct engine *engine = engine_luaget(L);
lua_newtable(L);
for (unsigned i = 0; i < engine->modules.len; ++i) {
struct kr_module *module = engine->modules.at[i];
lua_pushstring(L, module->name);
lua_rawseti(L, -2, i + 1);
}
return 1;
}
/** Load module. */
static int mod_load(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n != 1 || !lua_isstring(L, 1)) {
format_error(L, "expected 'load(string name)'");
lua_error(L);
}
/* Load engine module */
struct engine *engine = engine_luaget(L);
int ret = engine_register(engine, lua_tostring(L, 1));
if (ret != 0) {
format_error(L, kr_strerror(ret));
lua_error(L);
}
lua_pushboolean(L, 1);
return 1;
}
/** Unload module. */
static int mod_unload(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n != 1 || !lua_isstring(L, 1)) {
format_error(L, "expected 'unload(string name)'");
lua_error(L);
}
/* Unload engine module */
struct engine *engine = engine_luaget(L);
int ret = engine_unregister(engine, lua_tostring(L, 1));
if (ret != 0) {
format_error(L, kr_strerror(ret));
lua_error(L);
}
lua_pushboolean(L, 1);
return 1;
}
int lib_modules(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "list", mod_list },
{ "load", mod_load },
{ "unload", mod_unload },
{ NULL, NULL }
};
register_lib(L, "modules", lib);
return 1;
}
/** Append 'addr = {port = int, udp = bool, tcp = bool}' */
static int net_list_add(const char *key, void *val, void *ext)
{
lua_State *L = (lua_State *)ext;
endpoint_array_t *ep_array = val;
lua_newtable(L);
for (size_t i = ep_array->len; i--;) {
struct endpoint *ep = ep_array->at[i];
lua_pushinteger(L, ep->port);
lua_setfield(L, -2, "port");
lua_pushboolean(L, ep->flags & NET_UDP);
lua_setfield(L, -2, "udp");
lua_pushboolean(L, ep->flags & NET_TCP);
lua_setfield(L, -2, "tcp");
}
lua_setfield(L, -2, key);
return kr_ok();
}
/** List active endpoints. */
static int net_list(lua_State *L)
{
struct engine *engine = engine_luaget(L);
lua_newtable(L);
map_walk(&engine->net.endpoints, net_list_add, L);
return 1;
}
/** Listen on interface address list. */
static int net_listen_iface(lua_State *L, int port)
{
/* Expand 'addr' key if exists */
lua_getfield(L, 1, "addr");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_pushvalue(L, 1);
}
/* Bind to address list */
struct engine *engine = engine_luaget(L);
size_t count = lua_rawlen(L, -1);
for (size_t i = 0; i < count; ++i) {
lua_rawgeti(L, -1, i + 1);
int ret = network_listen(&engine->net, lua_tostring(L, -1),
port, NET_TCP|NET_UDP);
if (ret != 0) {
format_error(L, kr_strerror(ret));
lua_error(L);
}
lua_pop(L, 1);
}
lua_pushboolean(L, true);
return 1;
}
/** Listen on endpoint. */
static int net_listen(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
int port = KR_DNS_PORT;
if (n > 1 && lua_isnumber(L, 2)) {
port = lua_tointeger(L, 2);
}
/* Process interface or (address, port) pair. */
if (lua_istable(L, 1)) {
return net_listen_iface(L, port);
} else if (n < 1 || !lua_isstring(L, 1)) {
format_error(L, "expected 'listen(string addr, number port = 53)'");
lua_error(L);
}
/* Open resolution context cache */
struct engine *engine = engine_luaget(L);
int ret = network_listen(&engine->net, lua_tostring(L, 1), port, NET_TCP|NET_UDP);
if (ret != 0) {
format_error(L, kr_strerror(ret));
lua_error(L);
}
lua_pushboolean(L, true);
return 1;
}
/** Close endpoint. */
static int net_close(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2) {
format_error(L, "expected 'close(string addr, number port)'");
lua_error(L);
}
/* Open resolution context cache */
struct engine *engine = engine_luaget(L);
int ret = network_close(&engine->net, lua_tostring(L, 1), lua_tointeger(L, 2));
lua_pushboolean(L, ret == 0);
return 1;
}
/** List available interfaces. */
static int net_interfaces(lua_State *L)
{
/* Retrieve interface list */
int count = 0;
char buf[INET6_ADDRSTRLEN]; /* http://tools.ietf.org/html/rfc4291 */
uv_interface_address_t *info = NULL;
uv_interface_addresses(&info, &count);
lua_newtable(L);
for (int i = 0; i < count; ++i) {
uv_interface_address_t iface = info[i];
lua_getfield(L, -1, iface.name);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
/* Address */
lua_getfield(L, -1, "addr");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
if (iface.address.address4.sin_family == AF_INET) {
uv_ip4_name(&iface.address.address4, buf, sizeof(buf));
} else if (iface.address.address4.sin_family == AF_INET6) {
uv_ip6_name(&iface.address.address6, buf, sizeof(buf));
} else {
buf[0] = '\0';
}
lua_pushstring(L, buf);
lua_rawseti(L, -2, lua_rawlen(L, -2) + 1);
lua_setfield(L, -2, "addr");
/* Hardware address. */
char *p = buf;
memset(buf, 0, sizeof(buf));
for (unsigned k = 0; k < sizeof(iface.phys_addr); ++k) {
sprintf(p, "%.2x:", iface.phys_addr[k] & 0xff);
p += 3;
}
*(p - 1) = '\0';
lua_pushstring(L, buf);
lua_setfield(L, -2, "mac");
/* Push table */
lua_setfield(L, -2, iface.name);
}
uv_free_interface_addresses(info, count);
return 1;
}
int lib_net(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "list", net_list },
{ "listen", net_listen },
{ "close", net_close },
{ "interfaces", net_interfaces },
{ NULL, NULL }
};
register_lib(L, "net", lib);
return 1;
}
/** Return available cached backends. */
static int cache_backends(lua_State *L)
{
struct engine *engine = engine_luaget(L);
storage_registry_t *registry = &engine->storage_registry;
lua_newtable(L);
for (unsigned i = 0; i < registry->len; ++i) {
struct storage_api *storage = &registry->at[i];
lua_pushboolean(L, storage->api() == engine->resolver.cache.api);
lua_setfield(L, -2, storage->prefix);
}
return 1;
}
/** Return number of cached records. */
static int cache_count(lua_State *L)
{
struct engine *engine = engine_luaget(L);
const namedb_api_t *storage = engine->resolver.cache.api;
/* Fetch item count */
struct kr_cache_txn txn;
int ret = kr_cache_txn_begin(&engine->resolver.cache, &txn, NAMEDB_RDONLY);
if (ret != 0) {
format_error(L, kr_strerror(ret));
lua_error(L);
}
lua_pushinteger(L, storage->count(&txn.t));
kr_cache_txn_abort(&txn);
return 1;
}
/** Return cache statistics. */
static int cache_stats(lua_State *L)
{
struct engine *engine = engine_luaget(L);
struct kr_cache *cache = &engine->resolver.cache;
lua_newtable(L);
lua_pushnumber(L, cache->stats.hit);
lua_setfield(L, -2, "hit");
lua_pushnumber(L, cache->stats.miss);
lua_setfield(L, -2, "miss");
lua_pushnumber(L, cache->stats.insert);
lua_setfield(L, -2, "insert");
lua_pushnumber(L, cache->stats.delete);
lua_setfield(L, -2, "delete");
lua_pushnumber(L, cache->stats.txn_read);
lua_setfield(L, -2, "txn_read");
lua_pushnumber(L, cache->stats.txn_write);
lua_setfield(L, -2, "txn_write");
return 1;
}
static struct storage_api *cache_select_storage(struct engine *engine, const char **conf)
{
/* Return default backend */
storage_registry_t *registry = &engine->storage_registry;
if (!*conf || !strstr(*conf, "://")) {
return &registry->at[0];
}
/* Find storage backend from config prefix */
for (unsigned i = 0; i < registry->len; ++i) {
struct storage_api *storage = &registry->at[i];
if (strncmp(*conf, storage->prefix, strlen(storage->prefix)) == 0) {
*conf += strlen(storage->prefix);
return storage;
}
}
return NULL;
}
/** Open cache */
static int cache_open(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 1 || !lua_isnumber(L, 1)) {
format_error(L, "expected 'open(number max_size, string config = \"\")'");
lua_error(L);
}
/* Select cache storage backend */
struct engine *engine = engine_luaget(L);
unsigned cache_size = lua_tonumber(L, 1);
const char *conf = n > 1 ? lua_tostring(L, 2) : NULL;
const char *uri = conf;
struct storage_api *storage = cache_select_storage(engine, &conf);
if (!storage) {
format_error(L, "unsupported cache backend");
lua_error(L);
}
/* Close if already open */
kr_cache_close(&engine->resolver.cache);
/* Reopen cache */
void *storage_opts = storage->opts_create(conf, cache_size);
int ret = kr_cache_open(&engine->resolver.cache, storage->api(), storage_opts, engine->pool);
free(storage_opts);
if (ret != 0) {
format_error(L, "can't open cache");
lua_error(L);
}
/* Store current configuration */
lua_getglobal(L, "cache");
lua_pushstring(L, "current_size");
lua_pushnumber(L, cache_size);
lua_rawset(L, -3);
lua_pushstring(L, "current_storage");
lua_pushstring(L, uri);
lua_rawset(L, -3);
lua_pushboolean(L, 1);
return 1;
}
static int cache_close(lua_State *L)
{
struct engine *engine = engine_luaget(L);
kr_cache_close(&engine->resolver.cache);
lua_pushboolean(L, 1);
return 1;
}
int lib_cache(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "backends", cache_backends },
{ "count", cache_count },
{ "stats", cache_stats },
{ "open", cache_open },
{ "close", cache_close },
{ NULL, NULL }
};
register_lib(L, "cache", lib);
return 1;
}
static void event_free(uv_timer_t *timer)
{
struct worker_ctx *worker = timer->loop->data;
lua_State *L = worker->engine->L;
int ref = (intptr_t) timer->data;
luaL_unref(L, LUA_REGISTRYINDEX, ref);
free(timer);
}
static void event_callback(uv_timer_t *timer)
{
struct worker_ctx *worker = timer->loop->data;
lua_State *L = worker->engine->L;
/* Retrieve callback and execute */
int top = lua_gettop(L);
lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) timer->data);
lua_rawgeti(L, -1, 1);
lua_pushinteger(L, (intptr_t) timer->data);
int ret = engine_pcall(L, 1);
if (ret != 0) {
fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
}
/* Clear the stack, there may be event a/o enything returned */
lua_settop(L, top);
lua_gc(L, LUA_GCCOLLECT, 0);
/* Free callback if not recurrent or an error */
if (ret != 0 || uv_timer_get_repeat(timer) == 0) {
uv_close((uv_handle_t *)timer, (uv_close_cb) event_free);
}
}
static int event_sched(lua_State *L, unsigned timeout, unsigned repeat)
{
uv_timer_t *timer = malloc(sizeof(*timer));
if (!timer) {
format_error(L, "out of memory");
lua_error(L);
}
/* Start timer with the reference */
uv_loop_t *loop = uv_default_loop();
uv_timer_init(loop, timer);
int ret = uv_timer_start(timer, event_callback, timeout, repeat);
if (ret != 0) {
free(timer);
format_error(L, "couldn't start the event");
lua_error(L);
}
/* Save callback and timer in registry */
lua_newtable(L);
lua_pushvalue(L, 2);
lua_rawseti(L, -2, 1);
lua_pushlightuserdata(L, timer);
lua_rawseti(L, -2, 2);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
/* Save reference to the timer */
timer->data = (void *) (intptr_t)ref;
lua_pushinteger(L, ref);
return 1;
}
static int event_after(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2)) {
format_error(L, "expected 'after(number timeout, function)'");
lua_error(L);
}
return event_sched(L, lua_tonumber(L, 1), 0);
}
static int event_recurrent(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2)) {
format_error(L, "expected 'recurrent(number interval, function)'");
lua_error(L);
}
return event_sched(L, 0, lua_tonumber(L, 1));
}
static int event_cancel(lua_State *L)
{
int n = lua_gettop(L);
if (n < 1 || !lua_isnumber(L, 1)) {
format_error(L, "expected 'cancel(number event)'");
lua_error(L);
}
/* Fetch event if it exists */
lua_rawgeti(L, LUA_REGISTRYINDEX, lua_tointeger(L, 1));
if (!lua_istable(L, -1)) {
format_error(L, "event not exists");
lua_error(L);
}
/* Close the timer */
lua_rawgeti(L, -1, 2);
uv_handle_t *timer = lua_touserdata(L, -1);
uv_close(timer, (uv_close_cb) event_free);
return 0;
}
int lib_event(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "after", event_after },
{ "recurrent", event_recurrent },
{ "cancel", event_cancel },
{ NULL, NULL }
};
register_lib(L, "event", lib);
return 1;
}
static inline struct worker_ctx *wrk_luaget(lua_State *L) {
lua_getglobal(L, "__worker");
struct worker_ctx *worker = lua_touserdata(L, -1);
lua_pop(L, 1);
return worker;
}
static int wrk_resolve(lua_State *L)
{
struct worker_ctx *worker = wrk_luaget(L);
if (!worker) {
return 0;
}
/* Create query packet */
knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_EDNS_MAX_UDP_PAYLOAD, NULL);
if (!pkt) {
lua_pushstring(L, strerror(ENOMEM));
lua_error(L);
}
uint8_t dname[KNOT_DNAME_MAXLEN];
knot_dname_from_str(dname, lua_tostring(L, 1), sizeof(dname));
/* Check class and type */
uint16_t rrtype = lua_tointeger(L, 2);
if (!lua_isnumber(L, 2)) {
lua_pushstring(L, "invalid RR type");
lua_error(L);
}
uint16_t rrclass = lua_tointeger(L, 3);
if (!lua_isnumber(L, 3)) { /* Default class is IN */
rrclass = KNOT_CLASS_IN;
}
knot_pkt_put_question(pkt, dname, rrclass, rrtype);
knot_wire_set_rd(pkt->wire);
/* Add OPT RR */
pkt->opt_rr = mm_alloc(&pkt->mm, sizeof(*pkt->opt_rr));
if (!pkt->opt_rr) {
return kr_error(ENOMEM);
}
int ret = knot_edns_init(pkt->opt_rr, KR_EDNS_PAYLOAD, 0, KR_EDNS_VERSION, &pkt->mm);
if (ret != 0) {
knot_pkt_free(&pkt);
return 0;
}
/* Resolve it */
unsigned options = lua_tointeger(L, 4);
ret = worker_resolve(worker, pkt, options);
knot_pkt_free(&pkt);
lua_pushboolean(L, ret == 0);
return 1;
}
/** Return worker statistics. */
static int wrk_stats(lua_State *L)
{
struct worker_ctx *worker = wrk_luaget(L);
if (!worker) {
return 0;
}
lua_newtable(L);
lua_pushnumber(L, worker->stats.concurrent);
lua_setfield(L, -2, "concurrent");
lua_pushnumber(L, worker->stats.udp);
lua_setfield(L, -2, "udp");
lua_pushnumber(L, worker->stats.tcp);
lua_setfield(L, -2, "tcp");
lua_pushnumber(L, worker->stats.ipv6);
lua_setfield(L, -2, "ipv6");
lua_pushnumber(L, worker->stats.ipv4);
lua_setfield(L, -2, "ipv4");
return 1;
}
int lib_worker(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "resolve", wrk_resolve },
{ "stats", wrk_stats },
{ NULL, NULL }
};
register_lib(L, "worker", lib);
return 1;
}
/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Bindings to engine services, see \a http://www.lua.org/manual/5.2/manual.html#luaL_newlib for the reference.
*/
#pragma once
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include "daemon/engine.h"
/** @internal Compatibility wrapper for Lua 5.0 - 5.2 */
#if LUA_VERSION_NUM >= 502
#define register_lib(L, name, lib) \
luaL_newlib((L), (lib))
#else
#define lua_rawlen(L, obj) \
lua_objlen((L), (obj))
#define register_lib(L, name, lib) \
luaL_openlib((L), (name), (lib), 0)
/* Adapted from Lua 5.2.0 */
static inline void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup) {
luaL_checkstack(L, nup+1, "too many upvalues");
for (; l->name != NULL; l++) { /* fill the table with given functions */
int i;
lua_pushstring(L, l->name);
for (i = 0; i < nup; i++) /* copy upvalues to the top */
lua_pushvalue(L, -(nup+1));
lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */
lua_settable(L, -(nup + 3));
}
lua_pop(L, nup); /* remove upvalues */
}
#endif
/**
* Load 'modules' package.
* @param L scriptable
* @return number of packages to load
*/
int lib_modules(lua_State *L);
/**
* Load 'net' package.
* @param L scriptable
* @return number of packages to load
*/
int lib_net(lua_State *L);
/**
* Load 'cache' package.
* @param L scriptable
* @return number of packages to load
*/
int lib_cache(lua_State *L);
/**
* Load 'event' package.
* @param L scriptable
* @return number of packages to load
*/
int lib_event(lua_State *L);
/**
* Load worker API.
* @param L scriptable
* @return number of packages to load
*/
int lib_worker(lua_State *L);
\ No newline at end of file
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <lua.h>
/** Make all the bindings accessible from the lua state,
* .i.e. define those lua tables. */
void kr_bindings_register(lua_State *L);
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "daemon/bindings/impl.h"
/** @internal return cache, or throw lua error if not open */
static struct kr_cache * cache_assert_open(lua_State *L)
{
struct kr_cache *cache = &the_resolver->cache;
if (kr_fails_assert(cache) || !kr_cache_is_open(cache))
lua_error_p(L, "no cache is open yet, use cache.open() or cache.size, etc.");
return cache;
}
/** Return available cached backends. */
static int cache_backends(lua_State *L)
{
lua_newtable(L);
for (unsigned i = 0; i < the_engine->backends.len; ++i) {
const struct kr_cdb_api *api = the_engine->backends.at[i];
lua_pushboolean(L, api == the_resolver->cache.api);
lua_setfield(L, -2, api->name);
}
return 1;
}
/** Return number of cached records. */
static int cache_count(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
int count = cache->api->count(cache->db, &cache->stats);
if (count >= 0) {
/* First key is a version counter, omit it if nonempty. */
lua_pushinteger(L, count ? count - 1 : 0);
return 1;
}
return 0;
}
/** Return time of last checkpoint, or re-set it if passed `true`. */
static int cache_checkpoint(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
if (lua_gettop(L) == 0) { /* Return the current value. */
lua_newtable(L);
lua_pushnumber(L, cache->checkpoint_monotime);
lua_setfield(L, -2, "monotime");
lua_newtable(L);
lua_pushnumber(L, cache->checkpoint_walltime.tv_sec);
lua_setfield(L, -2, "sec");
lua_pushnumber(L, cache->checkpoint_walltime.tv_usec);
lua_setfield(L, -2, "usec");
lua_setfield(L, -2, "walltime");
return 1;
}
if (lua_gettop(L) != 1 || !lua_isboolean(L, 1) || !lua_toboolean(L, 1))
lua_error_p(L, "cache.checkpoint() takes no parameters or a true value");
kr_cache_make_checkpoint(cache);
return 1;
}
/** Return cache statistics. */
static int cache_stats(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
lua_newtable(L);
#define add_stat(name) \
lua_pushinteger(L, (cache->stats.name)); \
lua_setfield(L, -2, #name)
add_stat(open);
add_stat(close);
add_stat(count);
cache->stats.count_entries = cache->api->count(cache->db, &cache->stats);
add_stat(count_entries);
add_stat(clear);
add_stat(commit);
add_stat(read);
add_stat(read_miss);
add_stat(write);
add_stat(remove);
add_stat(remove_miss);
add_stat(match);
add_stat(match_miss);
add_stat(read_leq);
add_stat(read_leq_miss);
/* usage_percent statistics special case - double */
cache->stats.usage_percent = cache->api->usage_percent(cache->db);
lua_pushnumber(L, cache->stats.usage_percent);
lua_setfield(L, -2, "usage_percent");
#undef add_stat
return 1;
}
static const struct kr_cdb_api *cache_select(const char **conf)
{
/* Return default backend */
if (*conf == NULL || !strstr(*conf, "://")) {
return the_engine->backends.at[0];
}
/* Find storage backend from config prefix */
for (unsigned i = 0; i < the_engine->backends.len; ++i) {
const struct kr_cdb_api *api = the_engine->backends.at[i];
if (strncmp(*conf, api->name, strlen(api->name)) == 0) {
*conf += strlen(api->name) + strlen("://");
return api;
}
}
return NULL;
}
static int cache_max_ttl(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
int n = lua_gettop(L);
if (n > 0) {
if (!lua_isnumber(L, 1) || n > 1)
lua_error_p(L, "expected 'max_ttl(number ttl)'");
uint32_t min = cache->ttl_min;
int64_t ttl = lua_tointeger(L, 1);
if (ttl < 1 || ttl < min || ttl > TTL_MAX_MAX) {
lua_error_p(L,
"max_ttl must be larger than minimum TTL, and in range <1, "
STR(TTL_MAX_MAX) ">'");
}
cache->ttl_max = ttl;
}
lua_pushinteger(L, cache->ttl_max);
return 1;
}
static int cache_min_ttl(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
int n = lua_gettop(L);
if (n > 0) {
if (!lua_isnumber(L, 1))
lua_error_p(L, "expected 'min_ttl(number ttl)'");
uint32_t max = cache->ttl_max;
int64_t ttl = lua_tointeger(L, 1);
if (ttl < 0 || ttl > max || ttl > TTL_MAX_MAX) {
lua_error_p(L,
"min_ttl must be smaller than maximum TTL, and in range <0, "
STR(TTL_MAX_MAX) ">'");
}
cache->ttl_min = ttl;
}
lua_pushinteger(L, cache->ttl_min);
return 1;
}
/** Open cache */
static int cache_open(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 1 || !lua_isnumber(L, 1))
lua_error_p(L, "expected 'open(number max_size, string config = \"\")'");
/* Select cache storage backend */
lua_Integer csize_lua = lua_tointeger(L, 1);
if (!(csize_lua >= 8192 && csize_lua < SIZE_MAX)) { /* min. is basically arbitrary */
lua_error_p(L, "invalid cache size specified, it must be in range <8192, "
STR(SIZE_MAX) ">");
}
size_t cache_size = csize_lua;
const char *conf = n > 1 ? lua_tostring(L, 2) : NULL;
const char *uri = conf;
const struct kr_cdb_api *api = cache_select(&conf);
if (!api)
lua_error_p(L, "unsupported cache backend");
/* Close if already open */
kr_cache_close(&the_resolver->cache);
/* Reopen cache */
struct kr_cdb_opts opts = {
.is_cache = true,
.path = (conf && strlen(conf)) ? conf : ".",
.maxsize = cache_size,
};
int ret = kr_cache_open(&the_resolver->cache, api, &opts, &the_engine->pool);
if (ret != 0) {
char cwd[PATH_MAX];
get_workdir(cwd, sizeof(cwd));
return luaL_error(L, "can't open cache path '%s'; working directory '%s'; %s",
opts.path, cwd, kr_strerror(ret));
}
/* Let's check_health() every five seconds to avoid keeping old cache alive
* even in case of not having any work to do. */
ret = kr_cache_check_health(&the_resolver->cache, 5000);
if (ret != 0) {
kr_log_error(CACHE, "periodic health check failed (ignored): %s\n",
kr_strerror(ret));
}
/* Store current configuration */
lua_getglobal(L, "cache");
lua_pushstring(L, "current_size");
lua_pushnumber(L, cache_size);
lua_rawset(L, -3);
lua_pushstring(L, "current_storage");
lua_pushstring(L, uri);
lua_rawset(L, -3);
lua_pop(L, 1);
lua_pushboolean(L, 1);
return 1;
}
static int cache_close(lua_State *L)
{
struct kr_cache *cache = &the_resolver->cache;
if (!kr_cache_is_open(cache)) {
return 0;
}
kr_cache_close(cache);
lua_getglobal(L, "cache");
lua_pushstring(L, "current_size");
lua_pushnumber(L, 0);
lua_rawset(L, -3);
lua_pop(L, 1);
lua_pushboolean(L, 1);
return 1;
}
#if 0
/** @internal Prefix walk. */
static int cache_prefixed(struct kr_cache *cache, const char *prefix, bool exact_name,
knot_db_val_t keyval[][2], int maxcount)
{
/* Convert to domain name */
uint8_t buf[KNOT_DNAME_MAXLEN];
if (!knot_dname_from_str(buf, prefix, sizeof(buf))) {
return kr_error(EINVAL);
}
/* Start prefix search */
return kr_cache_match(cache, buf, exact_name, keyval, maxcount);
}
#endif
/** Clear everything. */
static int cache_clear_everything(lua_State *L)
{
struct kr_cache *cache = cache_assert_open(L);
/* Clear records and packets. */
int ret = kr_cache_clear(cache);
lua_error_maybe(L, ret);
/* Clear reputation tables */
lru_reset(the_resolver->cache_cookie);
lua_pushboolean(L, true);
return 1;
}
#if 0
/** @internal Dump cache key into table on Lua stack. */
static void cache_dump(lua_State *L, knot_db_val_t keyval[])
{
knot_dname_t dname[KNOT_DNAME_MAXLEN];
char name[KNOT_DNAME_TXT_MAXLEN];
uint16_t type;
int ret = kr_unpack_cache_key(keyval[0], dname, &type);
if (ret < 0) {
return;
}
ret = !knot_dname_to_str(name, dname, sizeof(name));
if (kr_fails_assert(!ret)) return;
/* If name typemap doesn't exist yet, create it */
lua_getfield(L, -1, name);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
/* Append to typemap */
char type_buf[KR_RRTYPE_STR_MAXLEN] = { '\0' };
knot_rrtype_to_string(type, type_buf, sizeof(type_buf));
lua_pushboolean(L, true);
lua_setfield(L, -2, type_buf);
/* Set name typemap */
lua_setfield(L, -2, name);
}
/** Query cached records. TODO: fix caveats in ./README.rst documentation? */
static int cache_get(lua_State *L)
{
//struct kr_cache *cache = cache_assert_open(L); // to be fixed soon
/* Check parameters */
int n = lua_gettop(L);
if (n < 1 || !lua_isstring(L, 1))
lua_error_p(L, "expected 'cache.get(string key)'");
/* Retrieve set of keys */
const char *prefix = lua_tostring(L, 1);
knot_db_val_t keyval[100][2];
int ret = cache_prefixed(cache, prefix, false/*FIXME*/, keyval, 100);
lua_error_maybe(L, ret);
/* Format output */
lua_newtable(L);
for (int i = 0; i < ret; ++i) {
cache_dump(L, keyval[i]);
}
return 1;
}
#endif
static int cache_get(lua_State *L)
{
lua_error_maybe(L, ENOSYS);
return kr_error(ENOSYS); /* doesn't happen */
}
/** Set time interval for cleaning rtt cache.
* Servers with score >= KR_NS_TIMEOUT will be cleaned after
* this interval ended up, so that they will be able to participate
* in NS elections again. */
static int cache_ns_tout(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 1) {
lua_pushinteger(L, the_resolver->cache_rtt_tout_retry_interval);
return 1;
}
if (!lua_isnumber(L, 1))
lua_error_p(L, "expected 'cache.ns_tout(interval in ms)'");
lua_Integer interval_lua = lua_tointeger(L, 1);
if (!(interval_lua > 0 && interval_lua < UINT_MAX)) {
lua_error_p(L, "invalid interval specified, it must be in range > 0, < "
STR(UINT_MAX));
}
the_resolver->cache_rtt_tout_retry_interval = interval_lua;
lua_pushinteger(L, the_resolver->cache_rtt_tout_retry_interval);
return 1;
}
int kr_bindings_cache(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "backends", cache_backends },
{ "count", cache_count },
{ "stats", cache_stats },
{ "checkpoint", cache_checkpoint },
{ "open", cache_open },
{ "close", cache_close },
{ "clear_everything", cache_clear_everything },
{ "get", cache_get },
{ "max_ttl", cache_max_ttl },
{ "min_ttl", cache_min_ttl },
{ "ns_tout", cache_ns_tout },
{ NULL, NULL }
};
luaL_register(L, "cache", lib);
return 1;
}
.. SPDX-License-Identifier: GPL-3.0-or-later
Cache
=====
Cache in Knot Resolver is stored on disk and also shared between
:ref:`systemd-multiple-instances` so resolver doesn't lose the cached data on
restart or crash.
To improve performance even further the resolver implements so-called aggressive caching
for DNSSEC-validated data (:rfc:`8198`), which improves performance and also protects
against some types of Random Subdomain Attacks.
.. _`cache_sizing`:
Sizing
------
For personal and small office use-cases cache size around 100 MB is more than enough.
For large deployments we recommend to run Knot Resolver on a dedicated machine,
and to allocate 90% of machine's free memory for resolver's cache.
.. note:: Choosing a cache size that can fit into RAM is important even if the
cache is stored on disk (default). Otherwise, the extra I/O caused by disk
access for missing pages can cause performance issues.
For example, imagine you have a machine with 16 GB of memory.
After machine restart you use command ``free -m`` to determine
amount of free memory (without swap):
.. code-block:: bash
$ free -m
total used free
Mem: 15907 979 14928
Now you can configure cache size to be 90% of the free memory 14 928 MB, i.e. 13 453 MB:
.. code-block:: lua
-- 90 % of free memory after machine restart
cache.size = 13453 * MB
It is also possible to set the cache size based on the file system size. This is useful
if you use a dedicated partition for cache (e.g. non-persistent tmpfs). It is recommended
to leave some free space for special files, such as locks.:
.. code-block:: lua
cache.size = cache.fssize() - 10*MB
.. note:: The `cache garbage collector <../deployment-advanced-no-systemd-processes.html#garbage-collector>`_
can be used to periodically trim the cache. It is enabled and configured by
default when running kresd with systemd integration.
.. _`cache_persistence`:
Persistence
-----------
.. tip:: Using tmpfs for cache improves performance and reduces disk I/O.
By default the cache is saved on a persistent storage device
so the content of the cache is persisted during system reboot.
This usually leads to smaller latency after restart etc.,
however in certain situations a non-persistent cache storage might be preferred, e.g.:
- Resolver handles high volume of queries and I/O performance to disk is too low.
- Threat model includes attacker getting access to disk content in power-off state.
- Disk has limited number of writes (e.g. flash memory in routers).
If non-persistent cache is desired configure cache directory to be on
tmpfs_ filesystem, a temporary in-memory file storage.
The cache content will be saved in memory, and thus have faster access
and will be lost on power-off or reboot.
.. note:: In most of the Unix-like systems ``/tmp`` and ``/var/run`` are
commonly mounted as tmpfs. While it is technically possible to move the
cache to an existing tmpfs filesystem, it is *not recommended*, since the
path to cache is configured in multiple places.
Mounting the cache directory as tmpfs_ is the recommended approach. Make sure
to use appropriate ``size=`` option and don't forget to adjust the size in the
config file as well.
.. code-block:: none
# /etc/fstab
tmpfs /var/cache/knot-resolver tmpfs rw,size=2G,uid=knot-resolver,gid=knot-resolver,nosuid,nodev,noexec,mode=0700 0 0
.. code-block:: lua
-- /etc/knot-resolver/kresd.conf
cache.size = cache.fssize() - 10*MB
.. _tmpfs: https://en.wikipedia.org/wiki/Tmpfs
Configuration reference
-----------------------
.. function:: cache.open(max_size[, config_uri])
:param number max_size: Maximum cache size in bytes.
:return: ``true`` if cache was opened
Open cache with a size limit. The cache will be reopened if already open.
Note that the max_size cannot be lowered, only increased due to how cache is implemented.
.. tip:: Use ``kB, MB, GB`` constants as a multiplier, e.g. ``100*MB``.
The URI ``lmdb://path`` allows you to change the cache directory.
Example:
.. code-block:: lua
cache.open(100 * MB, 'lmdb:///var/cache/knot-resolver')
.. envvar:: cache.size
Set the cache maximum size in bytes. Note that this is only a hint to the backend,
which may or may not respect it. See :func:`cache.open()`.
.. code-block:: lua
cache.size = 100 * MB -- equivalent to `cache.open(100 * MB)`
.. envvar:: cache.current_size
Get the maximum size in bytes.
.. code-block:: lua
print(cache.current_size)
.. envvar:: cache.storage
Set the cache storage backend configuration, see :func:`cache.backends()` for
more information. If the new storage configuration is invalid, it is not set.
.. code-block:: lua
cache.storage = 'lmdb://.'
.. envvar:: cache.current_storage
Get the storage backend configuration.
.. code-block:: lua
print(cache.current_storage)
.. function:: cache.backends()
:return: map of backends
.. note:: For now there is only one backend implementation, even though the APIs are ready for different (synchronous) backends.
The cache supports runtime-changeable backends, using the optional :rfc:`3986` URI, where the scheme
represents backend protocol and the rest of the URI backend-specific configuration. By default, it
is a ``lmdb`` backend in working directory, i.e. ``lmdb://``.
Example output:
.. code-block:: lua
[lmdb://] => true
.. function:: cache.count()
:return: Number of entries in the cache. Meaning of the number is an implementation detail and is subject of change.
.. function:: cache.close()
:return: ``true`` if cache was closed
Close the cache.
.. note:: This may or may not clear the cache, depending on the cache backend.
.. function:: cache.fssize()
:return: Partition size of cache storage.
.. function:: cache.stats()
Return table with low-level statistics for internal cache operation and storage.
This counts each access to cache and does not directly map to individual
DNS queries or resource records.
For query-level statistics see :ref:`stats module <mod-stats>`.
Example:
.. code-block:: lua
> cache.stats()
[clear] => 0
[close] => 0
[commit] => 117
[count] => 2
[count_entries] => 6187
[match] => 21
[match_miss] => 2
[open] => 0
[read] => 4313
[read_leq] => 9
[read_leq_miss] => 4
[read_miss] => 1143
[remove] => 17
[remove_miss] => 0
[usage_percent] => 15.625
[write] => 189
Cache operation `read_leq` (*read less or equal*, i.e. range search) was requested 9 times,
and 4 out of 9 operations were finished with *cache miss*.
Cache contains 6187 internal entries which occupy 15.625 % cache size.
.. function:: cache.max_ttl([ttl])
:param number ttl: maximum TTL in seconds (default: 1 day)
.. KR_CACHE_DEFAULT_TTL_MAX ^^
:return: current maximum TTL
Get or set upper TTL bound applied to all received records.
.. note:: The `ttl` value must be in range `(min_ttl, 2147483647)`.
.. code-block:: lua
-- Get maximum TTL
cache.max_ttl()
518400
-- Set maximum TTL
cache.max_ttl(172800)
172800
.. function:: cache.min_ttl([ttl])
:param number ttl: minimum TTL in seconds (default: 5 seconds)
.. KR_CACHE_DEFAULT_TTL_MIN ^^
:return: current minimum TTL
Get or set lower TTL bound applied to all received records.
Forcing TTL higher than specified violates DNS standards, so use higher values with care.
TTL still won't be extended beyond expiration of the corresponding DNSSEC signature.
.. note:: The `ttl` value must be in range `<0, max_ttl)`.
.. code-block:: lua
-- Get minimum TTL
cache.min_ttl()
0
-- Set minimum TTL
cache.min_ttl(5)
5
.. function:: cache.ns_tout([timeout])
:param number timeout: NS retry interval in milliseconds (default: :c:macro:`KR_NS_TIMEOUT_RETRY_INTERVAL`)
:return: current timeout
Get or set time interval for which a nameserver address will be ignored after determining that it doesn't return (useful) answers.
The intention is to avoid waiting if there's little hope; instead, kresd can immediately SERVFAIL or immediately use stale records (with :ref:`serve_stale <mod-serve_stale>` module).
.. warning:: This settings applies only to the current kresd process.
.. function:: cache.get([domain])
This function is not implemented at this moment.
We plan to re-introduce it soon, probably with a slightly different API.
.. function:: cache.clear([name], [exact_name], [rr_type], [chunk_size], [callback], [prev_state])
Purge cache records matching specified criteria. There are two specifics:
* To reliably remove **negative** cache entries you need to clear subtree with the whole zone. E.g. to clear negative cache entries for (formerly non-existing) record `www.example.com. A` you need to flush whole subtree starting at zone apex, e.g. `example.com.` [#]_.
* This operation is asynchronous and might not be yet finished when call to ``cache.clear()`` function returns. Return value indicates if clearing continues asynchronously or not.
:param string name: subtree to purge; if the name isn't provided, whole cache is purged
(and any other parameters are disregarded).
:param bool exact_name: if set to ``true``, only records with *the same* name are removed;
default: false.
:param kres.type rr_type: you may additionally specify the type to remove,
but that is only supported with ``exact_name == true``; default: nil.
:param integer chunk_size: the number of records to remove in one round; default: 100.
The purpose is not to block the resolver for long.
The default ``callback`` repeats the command after one millisecond
until all matching data are cleared.
:param function callback: a custom code to handle result of the underlying C call.
Its parameters are copies of those passed to `cache.clear()` with one additional
parameter ``rettable`` containing table with return value from current call.
``count`` field contains a return code from :func:`kr_cache_remove_subtree()`.
:param table prev_state: return value from previous run (can be used by callback)
:rtype: table
:return: ``count`` key is always present. Other keys are optional and their presence indicate special conditions.
* **count** *(integer)* - number of items removed from cache by this call (can be 0 if no entry matched criteria)
* **not_apex** - cleared subtree is not cached as zone apex; proofs of non-existence were probably not removed
* **subtree** *(string)* - hint where zone apex lies (this is estimation from cache content and might not be accurate)
* **chunk_limit** - more than ``chunk_size`` items needs to be cleared, clearing will continue asynchronously
Examples:
.. code-block:: lua
-- Clear whole cache
> cache.clear()
[count] => 76
-- Clear records at and below 'com.'
> cache.clear('com.')
[chunk_limit] => chunk size limit reached; the default callback will continue asynchronously
[not_apex] => to clear proofs of non-existence call cache.clear('com.')
[count] => 100
[round] => 1
[subtree] => com.
> worker.sleep(0.1)
[cache] asynchronous cache.clear('com', false) finished
-- Clear only 'www.example.com.'
> cache.clear('www.example.com.', true)
[round] => 1
[count] => 1
[not_apex] => to clear proofs of non-existence call cache.clear('example.com.')
[subtree] => example.com.
.. [#] This is a consequence of DNSSEC negative cache which relies on proofs of non-existence on various owner nodes. It is impossible to efficiently flush part of DNS zones signed with NSEC3.
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "daemon/bindings/impl.h"
#include <unistd.h>
#include <uv.h>
static void event_free(uv_timer_t *timer)
{
lua_State *L = the_engine->L;
int ref = (intptr_t) timer->data;
luaL_unref(L, LUA_REGISTRYINDEX, ref);
free(timer);
}
static void event_callback(uv_timer_t *timer)
{
lua_State *L = the_engine->L;
/* Retrieve callback and execute */
lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) timer->data);
lua_rawgeti(L, -1, 1);
lua_pushinteger(L, (intptr_t) timer->data);
int ret = execute_callback(L, 1);
/* Free callback if not recurrent or an error */
if (ret != 0 || (uv_timer_get_repeat(timer) == 0 && uv_is_active((uv_handle_t *)timer) == 0)) {
if (!uv_is_closing((uv_handle_t *)timer)) {
uv_close((uv_handle_t *)timer, (uv_close_cb) event_free);
}
}
}
static void event_fdcallback(uv_poll_t* handle, int status, int events)
{
lua_State *L = the_engine->L;
/* Retrieve callback and execute */
lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) handle->data);
lua_rawgeti(L, -1, 1);
lua_pushinteger(L, (intptr_t) handle->data);
lua_pushinteger(L, status);
lua_pushinteger(L, events);
int ret = execute_callback(L, 3);
/* Free callback if not recurrent or an error */
if (ret != 0) {
if (!uv_is_closing((uv_handle_t *)handle)) {
uv_close((uv_handle_t *)handle, (uv_close_cb) event_free);
}
}
}
static int event_sched(lua_State *L, unsigned timeout, unsigned repeat)
{
uv_timer_t *timer = malloc(sizeof(*timer));
if (!timer)
lua_error_p(L, "out of memory");
/* Start timer with the reference */
uv_loop_t *loop = uv_default_loop();
int ret = uv_timer_init(loop, timer);
if (ret != 0)
goto exit_err;
ret = uv_timer_start(timer, event_callback, timeout, repeat);
if (ret != 0)
goto exit_err;
/* Save callback and timer in registry */
lua_newtable(L);
lua_pushvalue(L, 2);
lua_rawseti(L, -2, 1);
lua_pushpointer(L, timer);
lua_rawseti(L, -2, 2);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
/* Save reference to the timer */
timer->data = (void *) (intptr_t)ref;
lua_pushinteger(L, ref);
return 1;
exit_err:
free(timer);
lua_error_p(L, "couldn't start the event");
}
static int event_after(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2))
lua_error_p(L, "expected 'after(number timeout, function)'");
return event_sched(L, lua_tointeger(L, 1), 0);
}
static int event_recurrent(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || lua_tointeger(L, 1) == 0
|| !lua_isfunction(L, 2))
lua_error_p(L, "expected 'recurrent(number interval, function)'");
return event_sched(L, 0, lua_tointeger(L, 1));
}
static int event_cancel(lua_State *L)
{
int n = lua_gettop(L);
if (n < 1 || !lua_isnumber(L, 1))
lua_error_p(L, "expected 'cancel(number event)'");
/* Fetch event if it exists */
lua_rawgeti(L, LUA_REGISTRYINDEX, lua_tointeger(L, 1));
bool ok = lua_istable(L, -1);
/* Close the timer */
uv_handle_t **timer_pp = NULL;
if (ok) {
lua_rawgeti(L, -1, 2);
timer_pp = lua_touserdata(L, -1);
ok = timer_pp && *timer_pp;
/* That have been sufficient safety checks, hopefully. */
}
if (ok && !uv_is_closing(*timer_pp)) {
uv_close(*timer_pp, (uv_close_cb)event_free);
}
lua_pushboolean(L, ok);
return 1;
}
static int event_reschedule(lua_State *L)
{
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || !lua_isnumber(L, 2))
lua_error_p(L, "expected 'reschedule(number event, number timeout)'");
/* Fetch event if it exists */
lua_rawgeti(L, LUA_REGISTRYINDEX, lua_tointeger(L, 1));
bool ok = lua_istable(L, -1);
/* Reschedule the timer */
uv_handle_t **timer_pp = NULL;
if (ok) {
lua_rawgeti(L, -1, 2);
timer_pp = lua_touserdata(L, -1);
ok = timer_pp && *timer_pp;
/* That have been sufficient safety checks, hopefully. */
}
if (ok && !uv_is_closing(*timer_pp)) {
int ret = uv_timer_start((uv_timer_t *)*timer_pp,
event_callback, lua_tointeger(L, 2), 0);
if (ret != 0) {
uv_close(*timer_pp, (uv_close_cb)event_free);
ok = false;
}
}
lua_pushboolean(L, ok);
return 1;
}
static int event_fdwatch(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2))
lua_error_p(L, "expected 'socket(number fd, function)'");
uv_poll_t *handle = malloc(sizeof(*handle));
if (!handle)
lua_error_p(L, "out of memory");
/* Start timer with the reference */
int sock = lua_tointeger(L, 1);
uv_loop_t *loop = uv_default_loop();
int ret = uv_poll_init(loop, handle, sock);
if (ret == 0)
ret = uv_poll_start(handle, UV_READABLE, event_fdcallback);
if (ret != 0) {
free(handle);
lua_error_p(L, "couldn't start event poller");
}
/* Save callback and timer in registry */
lua_newtable(L);
lua_pushvalue(L, 2);
lua_rawseti(L, -2, 1);
lua_pushpointer(L, handle);
lua_rawseti(L, -2, 2);
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
/* Save reference to the timer */
handle->data = (void *) (intptr_t)ref;
lua_pushinteger(L, ref);
return 1;
}
int kr_bindings_event(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "after", event_after },
{ "recurrent", event_recurrent },
{ "cancel", event_cancel },
{ "socket", event_fdwatch },
{ "reschedule", event_reschedule },
{ NULL, NULL }
};
luaL_register(L, "event", lib);
return 1;
}
.. SPDX-License-Identifier: GPL-3.0-or-later
Timers and events reference
^^^^^^^^^^^^^^^^^^^^^^^^^^^
The timer represents exactly the thing described in the examples - it allows you to execute closures_
after specified time, or event recurrent events. Time is always described in milliseconds,
but there are convenient variables that you can use - ``sec, minute, hour``.
For example, ``5 * hour`` represents five hours, or 5*60*60*100 milliseconds.
.. function:: event.after(time, function)
:return: event id
Execute function after the specified time has passed.
The first parameter of the callback is the event itself.
Example:
.. code-block:: lua
event.after(1 * minute, function() print('Hi!') end)
.. function:: event.recurrent(interval, function)
:return: event id
Execute function immediately and then periodically after each ``interval``.
Example:
.. code-block:: lua
msg_count = 0
event.recurrent(5 * sec, function(e)
msg_count = msg_count + 1
print('Hi #'..msg_count)
end)
.. function:: event.reschedule(event_id, timeout)
Reschedule a running event, it has no effect on canceled events.
New events may reuse the event_id, so the behaviour is undefined if the function
is called after another event is started.
Example:
.. code-block:: lua
local interval = 1 * minute
event.after(1 * minute, function (ev)
print('Good morning!')
-- Halve the interval for each iteration
interval = interval / 2
event.reschedule(ev, interval)
end)
.. function:: event.cancel(event_id)
Cancel running event, it has no effect on already canceled events.
New events may reuse the event_id, so the behaviour is undefined if the function
is called after another event is started.
Example:
.. code-block:: lua
e = event.after(1 * minute, function() print('Hi!') end)
event.cancel(e)
Watch for file descriptor activity. This allows embedding other event loops or simply
firing events when a pipe endpoint becomes active. In another words, asynchronous
notifications for daemon.
.. function:: event.socket(fd, cb)
:param number fd: file descriptor to watch
:param cb: closure or callback to execute when fd becomes active
:return: event id
Execute function when there is activity on the file descriptor and calls a closure
with event id as the first parameter, status as second and number of events as third.
Example:
.. code-block:: lua
e = event.socket(0, function(e, status, nevents)
print('activity detected')
end)
e.cancel(e)
Asynchronous function execution
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The `event` package provides a very basic mean for non-blocking execution - it allows running code when activity on a file descriptor is detected, and when a certain amount of time passes. It doesn't however provide an easy to use abstraction for non-blocking I/O. This is instead exposed through the `worker` package (if `cqueues` Lua package is installed in the system).
.. function:: worker.coroutine(function)
Start a new coroutine with given function (closure). The function can do I/O or run timers without blocking the main thread. See cqueues_ for documentation of possible operations and synchronization primitives. The main limitation is that you can't wait for a finish of a coroutine from processing layers, because it's not currently possible to suspend and resume execution of processing layers.
Example:
.. code-block:: lua
worker.coroutine(function ()
for i = 0, 10 do
print('executing', i)
worker.sleep(1)
end
end)
.. function:: worker.sleep(seconds)
Pause execution of current function (asynchronously if running inside a worker coroutine).
Example:
.. code-block:: lua
function async_print(testname, sleep)
log(testname .. ': system time before sleep' .. tostring(os.time())
worker.sleep(sleep) -- other coroutines continue execution now
log(testname .. ': system time AFTER sleep' .. tostring(os.time())
end
worker.coroutine(function() async_print('call #1', 5) end)
worker.coroutine(function() async_print('call #2', 3) end)
Output from this example demonstrates that both calls to function ``async_print`` were executed asynchronously:
.. code-block:: none
call #2: system time before sleep 1578065073
call #1: system time before sleep 1578065073
call #2: system time AFTER sleep 1578065076
call #1: system time AFTER sleep 1578065078
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <dirent.h>
#include <lua.h>
#include <lauxlib.h>
#include <string.h>
const char * lua_table_checkindices(lua_State *L, const char *keys[])
{
/* Iterate over table at the top of the stack.
* http://www.lua.org/manual/5.1/manual.html#lua_next */
for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
lua_pop(L, 1); /* we don't need the value */
/* We need to copy the key, as _tostring() confuses _next().
* https://www.lua.org/manual/5.1/manual.html#lua_tolstring */
lua_pushvalue(L, -1);
const char *key = lua_tostring(L, -1);
if (!key)
return "<NON-STRING_INDEX>";
for (const char **k = keys; ; ++k) {
if (*k == NULL)
return key;
if (strcmp(*k, key) == 0)
break;
}
}
return NULL;
}
/** Return table listing filenames in a given directory (ls -A). */
static int kluautil_list_dir(lua_State *L)
{
lua_newtable(L); // empty table even on errors
const char *path = lua_tolstring(L, 1, NULL);
if (!path) return 1;
DIR *dir = opendir(path);
if (!dir) return 1;
struct dirent *entry;
int lua_i = 1;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) {
lua_pushstring(L, entry->d_name);
lua_rawseti(L, -2, lua_i++);
}
}
closedir(dir);
return 1;
}
/* Each of these just creates the correspondingly named lua table of functions. */
int kr_bindings_cache (lua_State *L); /* ./cache.c */
int kr_bindings_event (lua_State *L); /* ./event.c */
int kr_bindings_modules (lua_State *L); /* ./modules.c */
int kr_bindings_net (lua_State *L); /* ./net.c */
int kr_bindings_worker (lua_State *L); /* ./worker.c */
void kr_bindings_register(lua_State *L)
{
kr_bindings_cache(L);
kr_bindings_event(L);
kr_bindings_modules(L);
kr_bindings_net(L);
kr_bindings_worker(L);
/* Finally some lua utils *written in C*, not really a binding. */
lua_register(L, "kluautil_list_dir", kluautil_list_dir);
}
void lua_error_p(lua_State *L, const char *fmt, ...)
{
/* Add a stack trace and throw the result as a lua error. */
luaL_traceback(L, L, "error occurred here (config filename:lineno is at the bottom, if config is involved):", 0);
/* Push formatted custom message, prepended with "ERROR: ". */
lua_pushliteral(L, "\nERROR: ");
{
va_list args;
va_start(args, fmt);
lua_pushvfstring(L, fmt, args);
va_end(args);
}
lua_concat(L, 3);
lua_error(L);
/* TODO: we might construct a little more friendly trace by using luaL_where().
* In particular, in case the error happens in a function that was called
* directly from a config file (the most common case), there isn't much need
* to format the trace in this heavy way. */
}
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include "daemon/engine.h"
#include "daemon/worker.h" /* the_worker is often useful */
#include <lua.h>
#include <lauxlib.h>
/* It may happen that include files are messed up and we're hitting a header
* e.g. from vanilla Lua. Even 5.1 won't work due to missing luaL_traceback() in <lauxlib.h>. */
#if (LUA_VERSION_NUM) != 501 || !defined(LUA_LJDIR)
#error "Incorrect Lua version in #include <lua.h> - LuaJIT compatible with Lua 5.1 is required"
#endif
/** Useful to stringify macros into error strings. */
#define STR(s) STRINGIFY_TOKEN(s)
#define STRINGIFY_TOKEN(s) #s
/** Check lua table at the top of the stack for allowed keys.
* \param keys NULL-terminated array of 0-terminated strings
* \return NULL if passed or the offending string (pushed on top of lua stack)
* \note Future work: if non-NULL is returned, there's extra stuff on the lua stack.
* \note Brute-force complexity: table length * summed length of keys.
*/
const char * lua_table_checkindices(lua_State *L, const char *keys[]);
/** If the value at the top of the stack isn't a table, make it a single-element list. */
static inline void lua_listify(lua_State *L)
{
if (lua_istable(L, -1))
return;
lua_createtable(L, 1, 0);
lua_insert(L, lua_gettop(L) - 1); /* swap the top two stack elements */
lua_pushinteger(L, 1);
lua_insert(L, lua_gettop(L) - 1); /* swap the top two stack elements */
lua_settable(L, -3);
}
/** Throw a formatted lua error.
*
* The message will get prefixed by "ERROR: " and supplemented by stack trace.
* \return never! It calls lua_error().
*
* Example:
ERROR: not a valid pin_sha256: 'a1Z/3ek=', raw length 5 instead of 32
stack traceback:
[C]: in function 'tls_client'
/PathToPREFIX/lib/kdns_modules/policy.lua:175: in function 'TLS_FORWARD'
/PathToConfig.lua:46: in main chunk
*/
KR_PRINTF(2) KR_NORETURN KR_COLD
void lua_error_p(lua_State *L, const char *fmt, ...);
/** @internal Annotate for static checkers. */
KR_NORETURN int lua_error(lua_State *L);
/** Shortcut for common case. */
static inline void lua_error_maybe(lua_State *L, int err)
{
if (err) lua_error_p(L, "%s", kr_strerror(err));
}
static inline int execute_callback(lua_State *L, int argc)
{
int ret = engine_pcall(L, argc);
if (ret != 0) {
kr_log_error(SYSTEM, "error: %s\n", lua_tostring(L, -1));
}
/* Clear the stack, there may be event a/o anything returned */
lua_settop(L, 0);
return ret;
}
/** Push a pointer as heavy/full userdata.
*
* It's useful as a replacement of lua_pushlightuserdata(),
* but note that it behaves differently in lua (converts to pointer-to-pointer).
*/
static inline void lua_pushpointer(lua_State *L, void *p)
{
void **addr = lua_newuserdata(L, sizeof(void *));
kr_require(addr);
memcpy(addr, &p, sizeof(void *));
}
/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <libknot/descriptor.h>
#include "daemon/bindings/kres.h"
#include "daemon/bindings.h"
#define WRAP_NUMBER(L, name, val) \
lua_pushnumber((L), (val)); \
lua_setfield((L), -2, (name))
#define WRAP_CONST(L, name, prefix...) \
WRAP_NUMBER(L, #name, prefix ## name)
#define WRAP_LUT(L, prefix, table) \
lua_newtable(L); \
for (const lookup_table_t *elm = (table); elm->name; ++elm) { \
WRAP_NUMBER((L), elm->name, elm->id); \
} \
lua_setfield((L), -2, (prefix))
#define LUA_ERRSTR(L, errstr) \
lua_pushliteral(L, errstr); \
lua_error(L)
#define CHECK_UDATA(udata, L) \
lua_touserdata(L, 1); if (!udata) return 0
/** @internal Register metatable. */
static void lua_register_meta(lua_State *L, const luaL_Reg *funcs, const char *name)
{
luaL_newmetatable(L, name); \
luaL_setfuncs(L, funcs, 0); \
lua_pushvalue(L, -1); \
lua_setfield(L, -2, "__index"); \
lua_pop(L, 1);
}
/** @internal Shortcut for dname conversion. */
static inline void lua_pushdname(lua_State *L, const knot_dname_t *name)
{
char dname_str[KNOT_DNAME_MAXLEN];
knot_dname_to_str(dname_str, name, sizeof(dname_str));
lua_pushstring(L, dname_str);
}
/*
* Record types, since the libknot doesn't export them.
*/
#define RECORD_TYPES(X) \
X(A) X(NS) X(CNAME) X(SOA) X(PTR) X(HINFO) X(MINFO) X(MX) \
X(TXT) X(RP) X(AFSDB) X(RT) X(SIG) X(KEY) X(AAAA) X(LOC) \
X(SRV) X(NAPTR) X(KX) X(CERT) X(DNAME) X(OPT) X(APL) X(DS) \
X(SSHFP) X(IPSECKEY) X(RRSIG) X(NSEC) X(DNSKEY) X(DHCID) \
X(NSEC3) X(NSEC3PARAM) X(TLSA) X(CDS) X(CDNSKEY) X(SPF) \
X(NID) X(L32) X(L64) X(LP) X(EUI48) X(EUI64) X(TKEY) \
X(TSIG) X(IXFR) X(AXFR) X(ANY)
static lookup_table_t rrtype_names[] = {
#define X(rc) { KNOT_RRTYPE_ ## rc, #rc },
RECORD_TYPES(X)
#undef X
{ 0, NULL }
};
/*
* Record class names.
*/
#define RECORD_CLASS(X) X(IN) X(CH) X(NONE) X(ANY)
static lookup_table_t rrclass_names[] = {
#define X(rc) { KNOT_CLASS_ ## rc, #rc },
RECORD_CLASS(X)
#undef X
{ 0, NULL }
};
/*
* Packet interface
* @note Packets are always light userdata, use single pointers.
*/
#define WIRE_FLAGS(X) \
X(AA,aa) X(AD,ad) X(CD,cd) X(RD,rd) X(QR,qr) X(RA,ra) X(TC,tc)
enum {
#define X(flag, _) WIRE_ ## flag,
WIRE_FLAGS(X)
#undef X
};
static lookup_table_t wire_flag_names[] = {
#define X(flag, _) { WIRE_ ## flag, #flag },
WIRE_FLAGS(X)
#undef X
{ 0, NULL }
};
#define PKT_UDATA_CHECK(L) \
if (!lua_touserdata(L, 1)) { \
LUA_ERRSTR(L, "bad parameters, expected (pkt[, newvalue])"); \
}
static int pkt_flag(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) > 1 && lua_isnumber(L, 2)) {
int flag_id = lua_tonumber(L, 2);
switch(flag_id) {
#define X(flag, code) case WIRE_ ## flag: knot_wire_set_ ## code (pkt->wire); break;
WIRE_FLAGS(X)
#undef X
}
}
return 0;
}
static int pkt_opcode(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) > 1 && lua_isnumber(L, 2)) {
knot_wire_set_opcode(pkt->wire, lua_tonumber(L, 2));
}
lua_pushnumber(L, knot_wire_get_opcode(pkt->wire));
return 1;
}
static int pkt_rcode(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) > 1 && lua_isnumber(L, 2)) {
knot_wire_set_rcode(pkt->wire, lua_tonumber(L, 2));
}
lua_pushnumber(L, knot_wire_get_rcode(pkt->wire));
return 1;
}
static int pkt_qtype(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
lua_pushnumber(L, knot_pkt_qtype(pkt));
return 1;
}
static int pkt_qclass(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
lua_pushnumber(L, knot_pkt_qclass(pkt));
return 1;
}
static int pkt_qname(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
lua_pushdname(L, knot_pkt_qname(pkt));
return 1;
}
static int pkt_question(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) < 3) {
return 0;
}
/* Check parameters */
uint8_t dname[KNOT_DNAME_MAXLEN];
knot_dname_from_str(dname, lua_tostring(L, 2), sizeof(dname));
uint16_t rrtype = lua_tointeger(L, 3);
uint16_t rrclass = lua_tointeger(L, 4);
if (!lua_isnumber(L, 3)) {
LUA_ERRSTR(L, "invalid RR type");
}
if (!lua_isnumber(L, 4)) { /* Default class is IN */
rrclass = KNOT_CLASS_IN;
}
const knot_dname_t *qname = knot_pkt_qname(pkt);
if (pkt->rrset_count > 0 || (!qname || !knot_dname_is_equal(qname, dname))) {
KR_PKT_RECYCLE(pkt);
knot_pkt_put_question(pkt, dname, rrclass, rrtype);
pkt->parsed = pkt->size;
}
return 0;
}
static int pkt_begin(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (!lua_isnumber(L, 2) || lua_tonumber(L, 2) < pkt->current) {
LUA_ERRSTR(L, "bad parameters, expected packet section >= current");
}
knot_pkt_begin(pkt, lua_tointeger(L, 2));
return 0;
}
static int pkt_add(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) < 6) {
return 0;
}
/* Get parameters */
uint8_t dname[KNOT_DNAME_MAXLEN];
knot_dname_from_str(dname, lua_tostring(L, 2), sizeof(dname));
uint16_t rrclass = lua_tointeger(L, 3);
uint16_t rrtype = lua_tointeger(L, 4);
uint32_t ttl = lua_tointeger(L, 5);
size_t rdlen = 0;
const char *raw_data = lua_tolstring(L, 6, &rdlen);
/* Create empty RR */
knot_rrset_t rr;
knot_rrset_init(&rr, knot_dname_copy(dname, &pkt->mm), rrtype, rrclass);
/* Create RDATA */
knot_rdata_t rdata[knot_rdata_array_size(rdlen)];
knot_rdata_init(rdata, rdlen, (const uint8_t *)raw_data, ttl);
knot_rdataset_add(&rr.rrs, rdata, &pkt->mm);
/* Append RR */
int ret = knot_pkt_put(pkt, 0, &rr, KNOT_PF_FREE);
lua_pushboolean(L, ret == 0);
pkt->parsed = pkt->size;
return 1;
}
static int pkt_get(lua_State *L)
{
knot_pkt_t *pkt = CHECK_UDATA(pkt, L);
if (lua_gettop(L) < 3) {
return 0;
}
/* Get parameters */
uint16_t section_id = lua_tointeger(L, 2);
uint16_t index = lua_tointeger(L, 3);
/* Get RR */
const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
if (!sec || sec->count <= index) {
return 0;
}
const knot_rrset_t *rr = knot_pkt_rr(sec, index);
lua_newtable(L);
lua_pushdname(L, rr->owner);
lua_setfield(L, -2, "owner");
lua_pushnumber(L, rr->rclass);
lua_setfield(L, -2, "class");
lua_pushnumber(L, rr->type);
lua_setfield(L, -2, "type");
lua_pushnumber(L, knot_rrset_ttl(rr));
lua_setfield(L, -2, "ttl");
lua_pushlightuserdata(L, (void *)&rr->rrs);
lua_setfield(L, -2, "rdata");
return 1;
}
static int pkt_meta_register(lua_State *L)
{
static const luaL_Reg wrap[] = {
{ "flag", pkt_flag },
{ "rcode", pkt_rcode },
{ "opcode", pkt_opcode },
{ "qtype", pkt_qtype },
{ "qclass", pkt_qclass },
{ "qname", pkt_qname },
{ "question", pkt_question },
{ "begin", pkt_begin },
{ "add", pkt_add },
{ "get", pkt_get },
{ NULL, NULL }
};
lua_register_meta(L, wrap, META_PKT);
return 0;
}
/**
* Query interface.
* @note Query is a full userdata, use double pointers.
*/
static int query_qtype(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
lua_pushnumber(L, qry->stype);
return 1;
}
static int query_qclass(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
lua_pushnumber(L, qry->sclass);
return 1;
}
static int query_qname(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
lua_pushdname(L, qry->sname);
return 1;
}
static int query_flag(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
if (lua_gettop(L) < 2 || !lua_isnumber(L, 2)) {
return 0;
}
qry->flags |= lua_tointeger(L, 2);
return 0;
}
static int query_clear_flag(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
if (lua_gettop(L) < 2 || !lua_isnumber(L, 2)) {
return 0;
}
qry->flags &= ~lua_tointeger(L, 2);
return 0;
}
static int query_has_flag(lua_State *L)
{
struct kr_query *qry = CHECK_UDATA(qry, L);
if (lua_gettop(L) < 2 || !lua_isnumber(L, 2)) {
return 0;
}
lua_pushboolean(L, qry->flags & lua_tointeger(L, 2));
return 1;
}
static int query_current(lua_State *L)
{
struct kr_request *req = CHECK_UDATA(req, L);
lua_pushlightuserdata(L, kr_rplan_current(&req->rplan));
return 1;
}
static int query_resolved(lua_State *L)
{
struct kr_request *req = CHECK_UDATA(req, L);
lua_pushlightuserdata(L, TAIL(req->rplan.resolved));
return 1;
}
static int qry_meta_register(lua_State *L)
{
static const luaL_Reg wrap[] = {
{ "qtype", query_qtype },
{ "qclass", query_qclass },
{ "qname", query_qname },
{ "flag", query_flag },
{ "clear_flag", query_clear_flag },
{ "has_flag", query_has_flag },
{ NULL, NULL }
};
lua_getfield(L, -1, "query");
for (const luaL_Reg *reg = wrap; reg->name; ++reg) {
lua_pushcfunction(L, reg->func);
lua_setfield(L, -2, reg->name);
}
lua_pop(L, 1);
return 0;
}
int lib_kres(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "query_current", query_current },
{ "query_resolved", query_resolved },
{ NULL, NULL }
};
/* Create module and register functions */
register_lib(L, "kres", lib);
/* Register states */
WRAP_CONST(L, NOOP, KNOT_STATE_);
WRAP_CONST(L, CONSUME, KNOT_STATE_);
WRAP_CONST(L, PRODUCE, KNOT_STATE_);
WRAP_CONST(L, DONE, KNOT_STATE_);
WRAP_CONST(L, FAIL, KNOT_STATE_);
/* Register packet sections */
WRAP_CONST(L, ANSWER, KNOT_);
WRAP_CONST(L, AUTHORITY, KNOT_);
WRAP_CONST(L, ADDITIONAL, KNOT_);
/* Register RCODE, OPCODE */
WRAP_LUT(L, "rcode", knot_rcode_names);
WRAP_LUT(L, "type", rrtype_names);
WRAP_LUT(L, "class", rrclass_names);
WRAP_LUT(L, "opcode", knot_opcode_names);
WRAP_LUT(L, "wire", wire_flag_names);
WRAP_LUT(L, "query", query_flag_names);
/* Register metatables */
pkt_meta_register(L);
qry_meta_register(L);
return 1;
}
/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Lua-friendly bindings to resolver library parts,
* notably packet parsing and interpretation and operation on primitives like domain names.
*/
#pragma once
#include "daemon/bindings.h"
/* Metatable list */
#define META_PKT "kres.meta_pkt"
/**
* Load libkres library.
* @param L scriptable
* @return number of packages to load
*/
int lib_kres(lua_State *L);
\ No newline at end of file
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "daemon/bindings/impl.h"
/** List loaded modules */
static int mod_list(lua_State *L)
{
const module_array_t * const modules = engine_modules();
lua_newtable(L);
for (unsigned i = 0; i < modules->len; ++i) {
struct kr_module *module = modules->at[i];
lua_pushstring(L, module->name);
lua_rawseti(L, -2, i + 1);
}
return 1;
}
/** Load module. */
static int mod_load(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n != 1 || !lua_isstring(L, 1))
lua_error_p(L, "expected 'load(string name)'");
/* Parse precedence declaration */
char *declaration = strdup(lua_tostring(L, 1));
if (!declaration)
return kr_error(ENOMEM);
const char *name = strtok(declaration, " ");
const char *precedence = strtok(NULL, " ");
const char *ref = strtok(NULL, " ");
/* Load engine module */
int ret = engine_register(name, precedence, ref);
free(declaration);
if (ret != 0) {
if (ret == kr_error(EIDRM)) {
lua_error_p(L, "referenced module not found");
} else {
lua_error_maybe(L, ret);
}
}
lua_pushboolean(L, 1);
return 1;
}
/** Unload module. */
static int mod_unload(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n != 1 || !lua_isstring(L, 1))
lua_error_p(L, "expected 'unload(string name)'");
/* Unload engine module */
int ret = engine_unregister(lua_tostring(L, 1));
lua_error_maybe(L, ret);
lua_pushboolean(L, 1);
return 1;
}
int kr_bindings_modules(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "list", mod_list },
{ "load", mod_load },
{ "unload", mod_unload },
{ NULL, NULL }
};
luaL_register(L, "modules", lib);
return 1;
}
.. SPDX-License-Identifier: GPL-3.0-or-later
Modules
=======
Knot Resolver functionality consists of separate modules, which allow you
to mix-and-match features you need without slowing down operation
by features you do not use.
This practically means that you need to load module before using features contained in it, for example:
.. code-block:: lua
-- load module and make dnstap features available
modules.load('dnstap')
-- configure dnstap features
dnstap.config({
socket_path = "/tmp/dnstap.sock"
})
Obviously ordering matters, so you have to load module first and configure it after it is loaded.
Here is full reference manual for module configuration:
.. function:: modules.list()
:return: List of loaded modules.
.. function:: modules.load(name)
:param string name: Module name, e.g. "hints"
:return: ``true`` if modules was (or already is) loaded, error otherwise.
Load a module by name.
.. function:: modules.unload(name)
:param string name: Module name, e.g. "detect_time_jump"
:return: ``true`` if modules was unloaded, error otherwise.
Unload a module by name. This is useful for unloading modules loaded by default, mainly for debugging purposes.
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "daemon/bindings/impl.h"
#include "contrib/base64.h"
#include "contrib/cleanup.h"
#include "daemon/network.h"
#include "daemon/tls.h"
#include "lib/utils.h"
#include <stdlib.h>
#define PROXY_DATA_STRLEN (INET6_ADDRSTRLEN + 1 + 3 + 1)
/** Table and next index on top of stack -> append entries for given endpoint_array_t. */
static int net_list_add(const char *b_key, uint32_t key_len, trie_val_t *val, void *ext)
{
endpoint_array_t *ep_array = *val;
lua_State *L = (lua_State *)ext;
lua_Integer i = lua_tointeger(L, -1);
for (int j = 0; j < ep_array->len; ++j) {
struct endpoint *ep = &ep_array->at[j];
lua_newtable(L); // connection tuple
if (ep->flags.kind) {
lua_pushstring(L, ep->flags.kind);
} else if (ep->flags.http && ep->flags.tls) {
lua_pushliteral(L, "doh2");
} else if (ep->flags.tls) {
lua_pushliteral(L, "tls");
} else if (ep->flags.xdp) {
lua_pushliteral(L, "xdp");
} else {
lua_pushliteral(L, "dns");
}
lua_setfield(L, -2, "kind");
lua_newtable(L); // "transport" table
switch (ep->family) {
case AF_INET:
lua_pushliteral(L, "inet4");
break;
case AF_INET6:
lua_pushliteral(L, "inet6");
break;
case AF_XDP:
lua_pushliteral(L, "inet4+inet6"); // both UDP ports at once
break;
case AF_UNIX:
lua_pushliteral(L, "unix");
break;
default:
kr_assert(false);
lua_pushliteral(L, "invalid");
}
lua_setfield(L, -2, "family");
const char *ip_str_const = network_endpoint_key_str((struct endpoint_key *) b_key);
kr_require(ip_str_const);
auto_free char *ip_str = strdup(ip_str_const);
kr_require(ip_str);
char *hm = strchr(ip_str, '#');
if (hm) /* Omit port */
*hm = '\0';
lua_pushstring(L, ip_str);
if (ep->family == AF_INET || ep->family == AF_INET6) {
lua_setfield(L, -2, "ip");
lua_pushboolean(L, ep->flags.freebind);
lua_setfield(L, -2, "freebind");
} else if (ep->family == AF_UNIX) {
lua_setfield(L, -2, "path");
} else if (ep->family == AF_XDP) {
lua_setfield(L, -2, "interface");
lua_pushinteger(L, ep->nic_queue);
lua_setfield(L, -2, "nic_queue");
}
if (ep->family != AF_UNIX) {
lua_pushinteger(L, ep->port);
lua_setfield(L, -2, "port");
}
if (ep->family == AF_UNIX) {
lua_pushliteral(L, "stream");
} else if (ep->flags.sock_type == SOCK_STREAM) {
lua_pushliteral(L, "tcp");
} else if (ep->flags.sock_type == SOCK_DGRAM) {
lua_pushliteral(L, "udp");
} else {
kr_assert(false);
lua_pushliteral(L, "invalid");
}
lua_setfield(L, -2, "protocol");
lua_setfield(L, -2, "transport");
lua_settable(L, -3);
i++;
lua_pushinteger(L, i);
}
return kr_ok();
}
/** List active endpoints. */
static int net_list(lua_State *L)
{
lua_newtable(L);
lua_pushinteger(L, 1);
trie_apply_with_key(the_network->endpoints, net_list_add, L);
lua_pop(L, 1);
return 1;
}
/** Listen on an address list represented by the top of lua stack.
* \note flags.kind ownership is not transferred, and flags.sock_type doesn't make sense
* \return success */
static bool net_listen_addrs(lua_State *L, int port, endpoint_flags_t flags, int16_t nic_queue)
{
if (kr_fails_assert(flags.xdp || nic_queue == -1))
return false;
/* Case: table with 'addr' field; only follow that field directly. */
lua_getfield(L, -1, "addr");
if (!lua_isnil(L, -1)) {
lua_replace(L, -2);
} else {
lua_pop(L, 1);
}
/* Case: string, representing a single address. */
const char *str = lua_tostring(L, -1);
if (str != NULL) {
const bool is_unix = str[0] == '/';
int ret = 0;
if (!flags.kind && !flags.tls) { /* normal UDP or XDP */
flags.sock_type = SOCK_DGRAM;
ret = network_listen(str, port, nic_queue, flags);
}
if (!flags.kind && !flags.xdp && ret == 0) { /* common for TCP, DoT and DoH (v2) */
flags.sock_type = SOCK_STREAM;
ret = network_listen(str, port, nic_queue, flags);
}
if (flags.kind) {
flags.kind = strdup(flags.kind);
flags.sock_type = SOCK_STREAM; /* TODO: allow to override this? */
ret = network_listen(str, (is_unix ? 0 : port), nic_queue, flags);
}
if (ret == 0) return true; /* success */
if (is_unix) {
kr_log_error(NETWORK, "bind to '%s' (UNIX): %s\n",
str, kr_strerror(ret));
} else if (flags.xdp) {
const char *err_str = knot_strerror(ret);
if (ret == KNOT_ELIMIT) {
if ((strcmp(str, "::") == 0 || strcmp(str, "0.0.0.0") == 0)) {
err_str = "wildcard addresses not supported with XDP";
} else {
err_str = "address matched multiple network interfaces";
}
} else if (ret == kr_error(ENODEV)) {
err_str = "invalid address or interface name";
}
/* Notable OK strerror: KNOT_EPERM Operation not permitted */
if (nic_queue == -1) {
kr_log_error(NETWORK, "failed to initialize XDP for '%s@%d'"
" (nic_queue = <auto>): %s\n",
str, port, err_str);
} else {
kr_log_error(NETWORK, "failed to initialize XDP for '%s@%d'"
" (nic_queue = %d): %s\n",
str, port, nic_queue, err_str);
}
} else {
const char *stype = flags.sock_type == SOCK_DGRAM ? "UDP" : "TCP";
kr_log_error(NETWORK, "bind to '%s@%d' (%s): %s\n",
str, port, stype, kr_strerror(ret));
}
return false; /* failure */
}
/* Last case: table where all entries are added recursively. */
if (!lua_istable(L, -1))
lua_error_p(L, "bad type for address");
lua_pushnil(L);
while (lua_next(L, -2)) {
if (!net_listen_addrs(L, port, flags, nic_queue))
return false;
lua_pop(L, 1);
}
return true;
}
static bool table_get_flag(lua_State *L, int index, const char *key, bool def)
{
bool result = def;
lua_getfield(L, index, key);
if (lua_isboolean(L, -1)) {
result = lua_toboolean(L, -1);
}
lua_pop(L, 1);
return result;
}
/** Listen on endpoint. */
static int net_listen(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 1 || n > 3) {
lua_error_p(L, "expected one to three arguments; usage:\n"
"net.listen(addresses, [port = " STR(KR_DNS_PORT)
", flags = {tls = (port == " STR(KR_DNS_TLS_PORT) ")}])\n");
}
int port = KR_DNS_PORT;
if (n > 1) {
if (lua_isnumber(L, 2)) {
port = lua_tointeger(L, 2);
} else
if (!lua_isnil(L, 2)) {
lua_error_p(L, "wrong type of second parameter (port number)");
}
}
endpoint_flags_t flags = { 0 };
if (port == KR_DNS_TLS_PORT) {
flags.tls = true;
} else if (port == KR_DNS_DOH_PORT) {
flags.http = flags.tls = true;
}
int16_t nic_queue = -1;
if (n > 2 && !lua_isnil(L, 3)) {
if (!lua_istable(L, 3))
lua_error_p(L, "wrong type of third parameter (table expected)");
flags.tls = table_get_flag(L, 3, "tls", flags.tls);
flags.freebind = table_get_flag(L, 3, "freebind", false);
lua_getfield(L, 3, "kind");
const char *k = lua_tostring(L, -1);
if (k && strcasecmp(k, "dns") == 0) {
flags.tls = flags.http = false;
} else if (k && strcasecmp(k, "xdp") == 0) {
flags.tls = flags.http = false;
flags.xdp = true;
} else if (k && strcasecmp(k, "tls") == 0) {
flags.tls = true;
flags.http = false;
} else if (k && strcasecmp(k, "doh2") == 0) {
flags.tls = flags.http = true;
} else if (k) {
flags.kind = k;
if (strcasecmp(k, "doh") == 0) {
lua_error_p(L, "kind=\"doh\" was renamed to kind=\"doh_legacy\", switch to the new implementation with kind=\"doh2\" or update your config");
}
}
lua_getfield(L, 3, "nic_queue");
if (lua_isnumber(L, -1)) {
if (flags.xdp) {
nic_queue = lua_tointeger(L, -1);
} else {
lua_error_p(L, "nic_queue only supported with kind = 'xdp'");
}
} else if (!lua_isnil(L, -1)) {
lua_error_p(L, "wrong value of nic_queue (integer expected)");
}
}
/* Memory management of `kind` string is difficult due to longjmp etc.
* Pop will unreference the lua value, so we store it on C stack instead (!) */
const int kind_alen = flags.kind ? strlen(flags.kind) + 1 : 1 /* 0 length isn't C standard */;
char kind_buf[kind_alen];
if (flags.kind) {
memcpy(kind_buf, flags.kind, kind_alen);
flags.kind = kind_buf;
}
/* Now focus on the first argument. */
lua_settop(L, 1);
if (!net_listen_addrs(L, port, flags, nic_queue))
lua_error_p(L, "net.listen() failed to bind");
lua_pushboolean(L, true);
return 1;
}
/** Prints the specified `data` into the specified `dst` buffer. */
static char *proxy_data_to_string(int af, const struct net_proxy_data *data,
char *dst, size_t size)
{
kr_assert(size >= PROXY_DATA_STRLEN);
const void *in_addr = (af == AF_INET)
? (void *) &data->addr.ip4
: (void *) &data->addr.ip6;
char *cur = dst;
const char *ret = inet_ntop(af, in_addr, cur, size);
if (!ret)
return NULL;
cur += strlen(cur); /*< advance cursor to after the address */
*(cur++) = '/';
int masklen = snprintf(cur, 3 + 1, "%u", data->netmask);
cur[masklen] = '\0';
return dst;
}
/** Put all IP addresses from `trie` into the table at the top of the Lua stack.
* For each address, increment the integer at `i`. All addresses in `trie` must
* be from the specified `family`. */
static void net_proxy_addr_put(lua_State *L, int family, trie_t *trie, int *i)
{
char addrbuf[PROXY_DATA_STRLEN];
const char *addr;
trie_it_t *it;
for (it = trie_it_begin(trie); !trie_it_finished(it); trie_it_next(it)) {
lua_pushinteger(L, *i);
struct net_proxy_data *data = *trie_it_val(it);
addr = proxy_data_to_string(family, data,
addrbuf, sizeof(addrbuf));
lua_pushstring(L, addr);
lua_settable(L, -3);
*i += 1;
}
trie_it_free(it);
}
/** Allow PROXYv2 headers for IP address. */
static int net_proxy_allowed(lua_State *L)
{
int n = lua_gettop(L);
int i = 1;
const char *addr;
/* Return current state */
if (n == 0) {
lua_newtable(L);
i = 1;
if (the_network->proxy_all4) {
lua_pushinteger(L, i);
lua_pushstring(L, "0.0.0.0/0");
lua_settable(L, -3);
i += 1;
} else {
net_proxy_addr_put(L, AF_INET, the_network->proxy_addrs4, &i);
}
if (the_network->proxy_all6) {
lua_pushinteger(L, i);
lua_pushstring(L, "::/0");
lua_settable(L, -3);
i += 1;
} else {
net_proxy_addr_put(L, AF_INET6, the_network->proxy_addrs6, &i);
}
return 1;
}
if (n != 1)
lua_error_p(L, "net.proxy_allowed() takes one parameter (string or table)");
if (!lua_istable(L, 1) && !lua_isstring(L, 1))
lua_error_p(L, "net.proxy_allowed() argument must be string or table");
/* Reset allowed proxy addresses */
network_proxy_reset();
/* Add new proxy addresses */
if (lua_istable(L, 1)) {
for (i = 1; !lua_isnil(L, -1); i++) {
lua_pushinteger(L, i);
lua_gettable(L, 1);
if (lua_isnil(L, -1)) /* missing value - end iteration */
break;
if (!lua_isstring(L, -1))
lua_error_p(L, "net.proxy_allowed() argument may only contain strings");
addr = lua_tostring(L, -1);
int ret = network_proxy_allow(addr);
if (ret)
lua_error_p(L, "invalid argument");
}
} else if (lua_isstring(L, 1)) {
addr = lua_tostring(L, 1);
int ret = network_proxy_allow(addr);
if (ret)
lua_error_p(L, "invalid argument");
}
return 0;
}
/** Close endpoint. */
static int net_close(lua_State *L)
{
/* Check parameters */
const int n = lua_gettop(L);
bool ok = (n == 1 || n == 2) && lua_isstring(L, 1);
const char *addr = lua_tostring(L, 1);
int port;
if (ok && (n < 2 || lua_isnil(L, 2))) {
port = -1;
} else if (ok) {
ok = lua_isnumber(L, 2);
port = lua_tointeger(L, 2);
ok = ok && port >= 0 && port <= 65535;
}
if (!ok)
lua_error_p(L, "expected 'close(string addr, [number port])'");
int ret = network_close(addr, port);
lua_pushboolean(L, ret == 0);
return 1;
}
/** List available interfaces. */
static int net_interfaces(lua_State *L)
{
/* Retrieve interface list */
int count = 0;
char buf[INET6_ADDRSTRLEN]; /* https://tools.ietf.org/html/rfc4291 */
uv_interface_address_t *info = NULL;
uv_interface_addresses(&info, &count);
lua_newtable(L);
for (int i = 0; i < count; ++i) {
uv_interface_address_t iface = info[i];
lua_getfield(L, -1, iface.name);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
/* Address */
lua_getfield(L, -1, "addr");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
if (iface.address.address4.sin_family == AF_INET) {
uv_ip4_name(&iface.address.address4, buf, sizeof(buf));
} else if (iface.address.address4.sin_family == AF_INET6) {
uv_ip6_name(&iface.address.address6, buf, sizeof(buf));
} else {
buf[0] = '\0';
}
if (kr_sockaddr_link_local((struct sockaddr *) &iface.address)) {
/* Link-local IPv6: add %interface prefix */
auto_free char *str = NULL;
int ret = asprintf(&str, "%s%%%s", buf, iface.name);
kr_assert(ret > 0);
lua_pushstring(L, str);
} else {
lua_pushstring(L, buf);
}
lua_rawseti(L, -2, lua_objlen(L, -2) + 1);
lua_setfield(L, -2, "addr");
/* Hardware address. */
char *p = buf;
for (int k = 0; k < sizeof(iface.phys_addr); ++k) {
(void)sprintf(p, "%.2x:", (uint8_t)iface.phys_addr[k]);
p += 3;
}
p[-1] = '\0';
lua_pushstring(L, buf);
lua_setfield(L, -2, "mac");
/* Push table */
lua_setfield(L, -2, iface.name);
}
uv_free_interface_addresses(info, count);
return 1;
}
/** Set UDP maximum payload size. */
static int net_bufsize(lua_State *L)
{
const int argc = lua_gettop(L);
if (argc == 0) {
lua_pushinteger(L, knot_edns_get_payload(the_resolver->downstream_opt_rr));
lua_pushinteger(L, knot_edns_get_payload(the_resolver->upstream_opt_rr));
return 2;
}
if (argc == 1) {
int bufsize = lua_tointeger(L, 1);
if (bufsize < 512 || bufsize > UINT16_MAX)
lua_error_p(L, "bufsize must be within <512, " STR(UINT16_MAX) ">");
knot_edns_set_payload(the_resolver->downstream_opt_rr, (uint16_t)bufsize);
knot_edns_set_payload(the_resolver->upstream_opt_rr, (uint16_t)bufsize);
} else if (argc == 2) {
int bufsize_downstream = lua_tointeger(L, 1);
int bufsize_upstream = lua_tointeger(L, 2);
if (bufsize_downstream < 512 || bufsize_upstream < 512
|| bufsize_downstream > UINT16_MAX || bufsize_upstream > UINT16_MAX) {
lua_error_p(L, "bufsize must be within <512, " STR(UINT16_MAX) ">");
}
knot_edns_set_payload(the_resolver->downstream_opt_rr, (uint16_t)bufsize_downstream);
knot_edns_set_payload(the_resolver->upstream_opt_rr, (uint16_t)bufsize_upstream);
}
return 0;
}
/** Set TCP pipelining size. */
static int net_pipeline(lua_State *L)
{
if (!the_worker) {
return 0;
}
if (!lua_isnumber(L, 1)) {
lua_pushinteger(L, the_worker->tcp_pipeline_max);
return 1;
}
int len = lua_tointeger(L, 1);
if (len < 0 || len > UINT16_MAX)
lua_error_p(L, "tcp_pipeline must be within <0, " STR(UINT16_MAX) ">");
the_worker->tcp_pipeline_max = len;
lua_pushinteger(L, len);
return 1;
}
static int net_tls(lua_State *L)
{
if (kr_fails_assert(the_network)) {
return 0;
}
/* Only return current credentials. */
if (lua_gettop(L) == 0) {
/* No credentials configured yet. */
if (!the_network->tls_credentials) {
return 0;
}
lua_newtable(L);
lua_pushstring(L, the_network->tls_credentials->tls_cert);
lua_setfield(L, -2, "cert_file");
lua_pushstring(L, the_network->tls_credentials->tls_key);
lua_setfield(L, -2, "key_file");
return 1;
}
if ((lua_gettop(L) != 2) || !lua_isstring(L, 1) || !lua_isstring(L, 2))
lua_error_p(L, "net.tls takes two parameters: (\"cert_file\", \"key_file\")");
int r = tls_certificate_set(lua_tostring(L, 1), lua_tostring(L, 2));
lua_error_maybe(L, r);
lua_pushboolean(L, true);
return 1;
}
/** Configure HTTP headers for DoH requests. */
static int net_doh_headers(lua_State *L)
{
doh_headerlist_t *headers = &the_worker->doh_qry_headers;
int i;
const char *name;
/* Only return current configuration. */
if (lua_gettop(L) == 0) {
lua_newtable(L);
for (i = 0; i < headers->len; i++) {
lua_pushinteger(L, i + 1);
name = headers->at[i];
lua_pushlstring(L, name, strlen(name));
lua_settable(L, -3);
}
return 1;
}
if (lua_gettop(L) != 1)
lua_error_p(L, "net.doh_headers() takes one parameter (string or table)");
if (!lua_istable(L, 1) && !lua_isstring(L, 1))
lua_error_p(L, "net.doh_headers() argument must be string or table");
/* Clear existing headers. */
for (i = 0; i < headers->len; i++)
free((void *)headers->at[i]);
array_clear(*headers);
if (lua_istable(L, 1)) {
for (i = 1; !lua_isnil(L, -1); i++) {
lua_pushinteger(L, i);
lua_gettable(L, 1);
if (lua_isnil(L, -1)) /* missing value - end iteration */
break;
if (!lua_isstring(L, -1))
lua_error_p(L, "net.doh_headers() argument table can only contain strings");
name = lua_tostring(L, -1);
array_push(*headers, strdup(name));
}
} else if (lua_isstring(L, 1)) {
name = lua_tostring(L, 1);
array_push(*headers, strdup(name));
}
return 0;
}
/** Return a lua table with TLS authentication parameters.
* The format is the same as passed to policy.TLS_FORWARD();
* more precisely, it's in a compatible canonical form. */
static int tls_params2lua(lua_State *L, trie_t *params)
{
lua_newtable(L);
if (!params) /* Allowed special case. */
return 1;
trie_it_t *it;
size_t list_index = 0;
for (it = trie_it_begin(params); !trie_it_finished(it); trie_it_next(it)) {
/* Prepare table for the current address
* and its index in the returned list. */
lua_pushinteger(L, ++list_index);
lua_createtable(L, 0, 2);
/* Get the "addr#port" string... */
size_t ia_len;
const char *key = trie_it_key(it, &ia_len);
int af = AF_UNSPEC;
if (ia_len == 2 + sizeof(struct in_addr)) {
af = AF_INET;
} else if (ia_len == 2 + sizeof(struct in6_addr)) {
af = AF_INET6;
}
if (kr_fails_assert(key && af != AF_UNSPEC))
lua_error_p(L, "internal error: bad IP address");
uint16_t port;
memcpy(&port, key, sizeof(port));
port = ntohs(port);
const char *ia = key + sizeof(port);
char str[INET6_ADDRSTRLEN + 1 + 5 + 1];
size_t len = sizeof(str);
if (kr_fails_assert(kr_ntop_str(af, ia, port, str, &len) == kr_ok()))
lua_error_p(L, "internal error: bad IP address conversion");
/* ...and push it as [1]. */
lua_pushinteger(L, 1);
lua_pushlstring(L, str, len - 1 /* len includes '\0' */);
lua_settable(L, -3);
const tls_client_param_t *e = *trie_it_val(it);
if (kr_fails_assert(e))
lua_error_p(L, "internal problem - NULL entry for %s", str);
/* .hostname = */
if (e->hostname) {
lua_pushstring(L, e->hostname);
lua_setfield(L, -2, "hostname");
}
/* .ca_files = */
if (e->ca_files.len) {
lua_createtable(L, e->ca_files.len, 0);
for (size_t i = 0; i < e->ca_files.len; ++i) {
lua_pushinteger(L, i + 1);
lua_pushstring(L, e->ca_files.at[i]);
lua_settable(L, -3);
}
lua_setfield(L, -2, "ca_files");
}
/* .pin_sha256 = ... ; keep sane indentation via goto. */
if (!e->pins.len) goto no_pins;
lua_createtable(L, e->pins.len, 0);
for (size_t i = 0; i < e->pins.len; ++i) {
uint8_t pin_base64[TLS_SHA256_BASE64_BUFLEN];
int err = kr_base64_encode(e->pins.at[i], TLS_SHA256_RAW_LEN,
pin_base64, sizeof(pin_base64));
if (kr_fails_assert(err >= 0))
lua_error_p(L,
"internal problem when converting pin_sha256: %s",
kr_strerror(err));
lua_pushinteger(L, i + 1);
lua_pushlstring(L, (const char *)pin_base64, err);
/* pin_base64 isn't 0-terminated ^^^ */
lua_settable(L, -3);
}
lua_setfield(L, -2, "pin_sha256");
no_pins:/* .insecure = */
if (e->insecure) {
lua_pushboolean(L, true);
lua_setfield(L, -2, "insecure");
}
/* Now the whole table is pushed atop the returned list. */
lua_settable(L, -3);
}
trie_it_free(it);
return 1;
}
static inline int cmp_sha256(const void *p1, const void *p2)
{
return memcmp(*(char * const *)p1, *(char * const *)p2, TLS_SHA256_RAW_LEN);
}
static int net_tls_client(lua_State *L)
{
/* TODO idea: allow starting the lua table with *multiple* IP targets,
* meaning the authentication config should be applied to each.
*/
if (lua_gettop(L) == 0)
return tls_params2lua(L, the_network->tls_client_params);
/* Various basic sanity-checking. */
if (lua_gettop(L) != 1 || !lua_istable(L, 1))
lua_error_maybe(L, EINVAL);
/* check that only allowed keys are present */
{
const char *bad_key = lua_table_checkindices(L, (const char *[])
{ "1", "hostname", "ca_file", "pin_sha256", "insecure", "tls", NULL });
if (bad_key)
lua_error_p(L, "found unexpected key '%s'", bad_key);
}
/**** Phase 1: get the parameter into a C struct, incl. parse of CA files,
* regardless of the address-pair having an entry already. */
tls_client_param_t *newcfg = tls_client_param_new();
if (!newcfg)
lua_error_p(L, "out of memory or something like that :-/");
/* Shortcut for cleanup actions needed from now on. */
#define ERROR(...) do { \
free(newcfg); \
lua_error_p(L, __VA_ARGS__); \
} while (false)
/* .hostname - always accepted. */
lua_getfield(L, 1, "hostname");
if (!lua_isnil(L, -1)) {
const char *hn_str = lua_tostring(L, -1);
/* Convert to lower-case dname and back, for checking etc. */
knot_dname_t dname[KNOT_DNAME_MAXLEN];
if (!hn_str || !knot_dname_from_str(dname, hn_str, sizeof(dname)))
ERROR("invalid hostname");
knot_dname_to_lower(dname);
char *h = knot_dname_to_str_alloc(dname);
if (!h)
ERROR("%s", kr_strerror(ENOMEM));
/* Strip the final dot produced by knot_dname_*() */
h[strlen(h) - 1] = '\0';
newcfg->hostname = h;
}
lua_pop(L, 1);
/* .ca_file - it can be a list of paths, contrary to the name. */
bool has_ca_file = false;
lua_getfield(L, 1, "ca_file");
if (!lua_isnil(L, -1)) {
if (!newcfg->hostname)
ERROR("missing hostname but specifying ca_file");
lua_listify(L);
array_init(newcfg->ca_files); /*< placate apparently confused scan-build */
if (array_reserve(newcfg->ca_files, lua_objlen(L, -1)) != 0) /*< optim. */
ERROR("%s", kr_strerror(ENOMEM));
/* Iterate over table at the top of the stack.
* http://www.lua.org/manual/5.1/manual.html#lua_next */
for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
has_ca_file = true; /* deferred here so that {} -> false */
const char *ca_file = lua_tostring(L, -1);
if (!ca_file)
ERROR("ca_file contains a non-string");
/* Let gnutls process it immediately, so garbage gets detected. */
int ret = gnutls_certificate_set_x509_trust_file(
newcfg->credentials, ca_file, GNUTLS_X509_FMT_PEM);
if (ret < 0) {
ERROR("failed to import certificate file '%s': %s - %s\n",
ca_file, gnutls_strerror_name(ret),
gnutls_strerror(ret));
} else {
kr_log_debug(TLSCLIENT, "imported %d certs from file '%s'\n",
ret, ca_file);
}
ca_file = strdup(ca_file);
if (!ca_file || array_push(newcfg->ca_files, ca_file) < 0)
ERROR("%s", kr_strerror(ENOMEM));
}
/* Sort the strings for easier comparison later. */
if (newcfg->ca_files.len) {
qsort(&newcfg->ca_files.at[0], newcfg->ca_files.len,
array_member_size(newcfg->ca_files), strcmp_p);
}
}
lua_pop(L, 1);
/* .pin_sha256 */
lua_getfield(L, 1, "pin_sha256");
if (!lua_isnil(L, -1)) {
if (has_ca_file)
ERROR("mixing pin_sha256 with ca_file is not supported");
lua_listify(L);
array_init(newcfg->pins); /*< placate apparently confused scan-build */
if (array_reserve(newcfg->pins, lua_objlen(L, -1)) != 0) /*< optim. */
ERROR("%s", kr_strerror(ENOMEM));
/* Iterate over table at the top of the stack. */
for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) {
const char *pin = lua_tostring(L, -1);
if (!pin)
ERROR("pin_sha256 is not a string");
uint8_t *pin_raw = malloc(TLS_SHA256_RAW_LEN);
/* Push the string early to simplify error processing. */
if (kr_fails_assert(pin_raw && array_push(newcfg->pins, pin_raw) >= 0)) {
free(pin_raw);
ERROR("%s", kr_strerror(ENOMEM));
}
int ret = kr_base64_decode((const uint8_t *)pin, strlen(pin),
pin_raw, TLS_SHA256_RAW_LEN + 8);
if (ret < 0) {
ERROR("not a valid pin_sha256: '%s' (length %d), %s\n",
pin, (int)strlen(pin), knot_strerror(ret));
} else if (ret != TLS_SHA256_RAW_LEN) {
ERROR("not a valid pin_sha256: '%s', "
"raw length %d instead of "
STR(TLS_SHA256_RAW_LEN)"\n",
pin, ret);
}
}
/* Sort the raw strings for easier comparison later. */
if (newcfg->pins.len) {
qsort(&newcfg->pins.at[0], newcfg->pins.len,
array_member_size(newcfg->pins), cmp_sha256);
}
}
lua_pop(L, 1);
/* .insecure */
lua_getfield(L, 1, "insecure");
if (lua_isnil(L, -1)) {
if (!newcfg->hostname && !newcfg->pins.len)
ERROR("no way to authenticate and not set as insecure");
} else if (lua_isboolean(L, -1) && lua_toboolean(L, -1)) {
newcfg->insecure = true;
if (has_ca_file || newcfg->pins.len)
ERROR("set as insecure but provided authentication config");
} else {
ERROR("incorrect value in the 'insecure' field");
}
lua_pop(L, 1);
/* Init CAs from system trust store, if needed. */
if (!newcfg->insecure && !newcfg->pins.len && !has_ca_file) {
int ret = gnutls_certificate_set_x509_system_trust(newcfg->credentials);
if (ret <= 0) {
ERROR("failed to use system CA certificate store: %s",
ret ? gnutls_strerror(ret) : kr_strerror(ENOENT));
} else {
kr_log_debug(TLSCLIENT, "imported %d certs from system store\n",
ret);
}
}
#undef ERROR
/**** Phase 2: deal with the C authentication "table". */
/* Parse address and port. */
lua_pushinteger(L, 1);
lua_gettable(L, 1);
const char *addr_str = lua_tostring(L, -1);
if (!addr_str)
lua_error_p(L, "address is not a string");
char buf[INET6_ADDRSTRLEN + 1];
uint16_t port = 853;
const struct sockaddr *addr = NULL;
if (kr_straddr_split(addr_str, buf, &port) == kr_ok())
addr = kr_straddr_socket(buf, port, NULL);
/* Add newcfg into the C map, saving the original into oldcfg. */
if (!addr)
lua_error_p(L, "address '%s' could not be converted", addr_str);
tls_client_param_t **oldcfgp = tls_client_param_getptr(
&the_network->tls_client_params, addr, true);
free_const(addr);
if (!oldcfgp)
lua_error_p(L, "internal error when extending tls_client_params map");
tls_client_param_t *oldcfg = *oldcfgp;
*oldcfgp = newcfg; /* replace old config in trie with the new one */
/* If there was no original entry, it's easy! */
if (!oldcfg)
return 0;
/* Check for equality (newcfg vs. oldcfg), and print a warning if not equal.*/
const bool ok_h = (!newcfg->hostname && !oldcfg->hostname)
|| (newcfg->hostname && oldcfg->hostname && strcmp(newcfg->hostname, oldcfg->hostname) == 0);
bool ok_ca = newcfg->ca_files.len == oldcfg->ca_files.len;
for (int i = 0; ok_ca && i < newcfg->ca_files.len; ++i)
ok_ca = strcmp(newcfg->ca_files.at[i], oldcfg->ca_files.at[i]) == 0;
bool ok_pins = newcfg->pins.len == oldcfg->pins.len;
for (int i = 0; ok_pins && i < newcfg->pins.len; ++i)
ok_ca = memcmp(newcfg->pins.at[i], oldcfg->pins.at[i], TLS_SHA256_RAW_LEN) == 0;
const bool ok_insecure = newcfg->insecure == oldcfg->insecure;
if (!(ok_h && ok_ca && ok_pins && ok_insecure)) {
kr_log_warning(TLSCLIENT,
"warning: re-defining TLS authentication parameters for %s\n",
addr_str);
}
tls_client_param_unref(oldcfg);
return 0;
}
int net_tls_client_clear(lua_State *L)
{
/* One parameter: address -> convert it to a struct sockaddr. */
if (lua_gettop(L) != 1 || !lua_isstring(L, 1))
lua_error_p(L, "net.tls_client_clear() requires one parameter (\"address\")");
const char *addr_str = lua_tostring(L, 1);
char buf[INET6_ADDRSTRLEN + 1];
uint16_t port = 853;
const struct sockaddr *addr = NULL;
if (kr_straddr_split(addr_str, buf, &port) == kr_ok())
addr = kr_straddr_socket(buf, port, NULL);
if (!addr)
lua_error_p(L, "invalid IP address");
/* Do the actual removal. */
int r = tls_client_param_remove(the_network->tls_client_params, addr);
free_const(addr);
lua_error_maybe(L, r);
lua_pushboolean(L, true);
return 1;
}
static int net_tls_padding(lua_State *L)
{
/* Only return current padding. */
if (lua_gettop(L) == 0) {
if (the_resolver->tls_padding < 0) {
lua_pushboolean(L, true);
return 1;
} else if (the_resolver->tls_padding == 0) {
lua_pushboolean(L, false);
return 1;
}
lua_pushinteger(L, the_resolver->tls_padding);
return 1;
}
const char *errstr = "net.tls_padding parameter has to be true, false,"
" or a number between <0, " STR(MAX_TLS_PADDING) ">";
if (lua_gettop(L) != 1)
lua_error_p(L, "%s", errstr);
if (lua_isboolean(L, 1)) {
bool x = lua_toboolean(L, 1);
if (x) {
the_resolver->tls_padding = -1;
} else {
the_resolver->tls_padding = 0;
}
} else if (lua_isnumber(L, 1)) {
int padding = lua_tointeger(L, 1);
if ((padding < 0) || (padding > MAX_TLS_PADDING))
lua_error_p(L, "%s", errstr);
the_resolver->tls_padding = padding;
} else {
lua_error_p(L, "%s", errstr);
}
lua_pushboolean(L, true);
return 1;
}
/** Shorter salt can't contain much entropy. */
#define net_tls_sticket_MIN_SECRET_LEN 32
static int net_tls_sticket_secret_string(lua_State *L)
{
size_t secret_len;
const char *secret;
if (lua_gettop(L) == 0) {
/* Zero-length secret, implying random key. */
secret_len = 0;
secret = NULL;
} else {
if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) {
lua_error_p(L,
"net.tls_sticket_secret takes one parameter: (\"secret string\")");
}
secret = lua_tolstring(L, 1, &secret_len);
if (secret_len < net_tls_sticket_MIN_SECRET_LEN || !secret) {
lua_error_p(L, "net.tls_sticket_secret - the secret is shorter than "
STR(net_tls_sticket_MIN_SECRET_LEN) " bytes");
}
}
tls_session_ticket_ctx_destroy(the_network->tls_session_ticket_ctx);
the_network->tls_session_ticket_ctx =
tls_session_ticket_ctx_create(the_network->loop, secret, secret_len);
if (the_network->tls_session_ticket_ctx == NULL) {
lua_error_p(L,
"net.tls_sticket_secret_string - can't create session ticket context");
}
lua_pushboolean(L, true);
return 1;
}
static int net_tls_sticket_secret_file(lua_State *L)
{
if (lua_gettop(L) != 1 || !lua_isstring(L, 1)) {
lua_error_p(L,
"net.tls_sticket_secret_file takes one parameter: (\"file name\")");
}
const char *file_name = lua_tostring(L, 1);
if (strlen(file_name) == 0)
lua_error_p(L, "net.tls_sticket_secret_file - empty file name");
FILE *fp = fopen(file_name, "r");
if (fp == NULL) {
lua_error_p(L, "net.tls_sticket_secret_file - can't open file '%s': %s",
file_name, strerror(errno));
}
char secret_buf[TLS_SESSION_TICKET_SECRET_MAX_LEN];
const size_t secret_len = fread(secret_buf, 1, sizeof(secret_buf), fp);
int err = ferror(fp);
if (err) {
lua_error_p(L,
"net.tls_sticket_secret_file - error reading from file '%s': %s",
file_name, strerror(err));
}
if (secret_len < net_tls_sticket_MIN_SECRET_LEN) {
lua_error_p(L,
"net.tls_sticket_secret_file - file '%s' is shorter than "
STR(net_tls_sticket_MIN_SECRET_LEN) " bytes",
file_name);
}
if (fclose(fp) == EOF) {
lua_error_p(L,
"net.tls_sticket_secret_file - reading of file '%s' failed",
file_name);
}
tls_session_ticket_ctx_destroy(the_network->tls_session_ticket_ctx);
the_network->tls_session_ticket_ctx =
tls_session_ticket_ctx_create(the_network->loop, secret_buf, secret_len);
if (the_network->tls_session_ticket_ctx == NULL) {
lua_error_p(L,
"net.tls_sticket_secret_file - can't create session ticket context");
}
lua_pushboolean(L, true);
return 1;
}
static int net_outgoing(lua_State *L, int family)
{
union kr_sockaddr *addr;
if (family == AF_INET)
addr = (union kr_sockaddr*)&the_worker->out_addr4;
else
addr = (union kr_sockaddr*)&the_worker->out_addr6;
if (lua_gettop(L) == 0) { /* Return the current value. */
if (addr->ip.sa_family == AF_UNSPEC) {
lua_pushnil(L);
return 1;
}
if (kr_fails_assert(addr->ip.sa_family == family))
lua_error_p(L, "bad address family");
char addr_buf[INET6_ADDRSTRLEN];
int err;
if (family == AF_INET)
err = uv_ip4_name(&addr->ip4, addr_buf, sizeof(addr_buf));
else
err = uv_ip6_name(&addr->ip6, addr_buf, sizeof(addr_buf));
lua_error_maybe(L, err);
lua_pushstring(L, addr_buf);
return 1;
}
if ((lua_gettop(L) != 1) || (!lua_isstring(L, 1) && !lua_isnil(L, 1)))
lua_error_p(L, "net.outgoing_vX takes one address string parameter or nil");
if (lua_isnil(L, 1)) {
addr->ip.sa_family = AF_UNSPEC;
return 1;
}
const char *addr_str = lua_tostring(L, 1);
int err;
if (family == AF_INET)
err = uv_ip4_addr(addr_str, 0, &addr->ip4);
else
err = uv_ip6_addr(addr_str, 0, &addr->ip6);
if (err)
lua_error_p(L, "net.outgoing_vX: failed to parse the address");
lua_pushboolean(L, true);
return 1;
}
static int net_outgoing_v4(lua_State *L) { return net_outgoing(L, AF_INET); }
static int net_outgoing_v6(lua_State *L) { return net_outgoing(L, AF_INET6); }
static int net_update_timeout(lua_State *L, uint64_t *timeout, const char *name)
{
/* Only return current idle timeout. */
if (lua_gettop(L) == 0) {
lua_pushinteger(L, *timeout);
return 1;
}
if ((lua_gettop(L) != 1))
lua_error_p(L, "%s takes one parameter: (\"idle timeout\")", name);
if (lua_isnumber(L, 1)) {
int idle_timeout = lua_tointeger(L, 1);
if (idle_timeout <= 0)
lua_error_p(L, "%s parameter has to be positive number", name);
*timeout = idle_timeout;
} else {
lua_error_p(L, "%s parameter has to be positive number", name);
}
lua_pushboolean(L, true);
return 1;
}
static int net_tcp_in_idle(lua_State *L)
{
return net_update_timeout(L, &the_network->tcp.in_idle_timeout, "net.tcp_in_idle");
}
static int net_tls_handshake_timeout(lua_State *L)
{
return net_update_timeout(L, &the_network->tcp.tls_handshake_timeout, "net.tls_handshake_timeout");
}
static int net_bpf_set(lua_State *L)
{
if (lua_gettop(L) != 1 || !lua_isnumber(L, 1)) {
lua_error_p(L, "net.bpf_set(fd) takes one parameter:"
" the open file descriptor of a loaded BPF program");
}
#if __linux__
int progfd = lua_tointeger(L, 1);
if (progfd == 0) {
/* conversion error despite that fact
* that lua_isnumber(L, 1) has returned true.
* Real or stdin? */
lua_error_p(L, "failed to convert parameter");
}
lua_pop(L, 1);
if (network_set_bpf(progfd) == 0) {
lua_error_p(L, "failed to attach BPF program to some networks: %s",
kr_strerror(errno));
}
lua_pushboolean(L, 1);
return 1;
#endif
lua_error_p(L, "BPF is not supported on this operating system");
}
static int net_bpf_clear(lua_State *L)
{
if (lua_gettop(L) != 0)
lua_error_p(L, "net.bpf_clear() does not take any parameters");
#if __linux__
network_clear_bpf();
lua_pushboolean(L, 1);
return 1;
#endif
lua_error_p(L, "BPF is not supported on this operating system");
}
static int net_register_endpoint_kind(lua_State *L)
{
const int param_count = lua_gettop(L);
if (param_count != 1 && param_count != 2)
lua_error_p(L, "expected one or two parameters");
if (!lua_isstring(L, 1)) {
lua_error_p(L, "incorrect kind '%s'", lua_tostring(L, 1));
}
size_t kind_len;
const char *kind = lua_tolstring(L, 1, &kind_len);
/* Unregistering */
if (param_count == 1) {
void *val;
if (trie_del(the_network->endpoint_kinds, kind, kind_len, &val) == KNOT_EOK) {
const int fun_id = (intptr_t)val;
luaL_unref(L, LUA_REGISTRYINDEX, fun_id);
return 0;
}
lua_error_p(L, "attempt to unregister unknown kind '%s'\n", kind);
} /* else -> param_count == 2 */
/* Registering */
if (!lua_isfunction(L, 2)) {
lua_error_p(L, "second parameter: expected function but got %s\n",
lua_typename(L, lua_type(L, 2)));
}
const int fun_id = luaL_ref(L, LUA_REGISTRYINDEX);
/* ^^ The function is on top of the stack, incidentally. */
void **pp = trie_get_ins(the_network->endpoint_kinds, kind, kind_len);
if (!pp) lua_error_maybe(L, kr_error(ENOMEM));
if (*pp != NULL || !strcasecmp(kind, "dns") || !strcasecmp(kind, "tls"))
lua_error_p(L, "attempt to register known kind '%s'\n", kind);
*pp = (void *)(intptr_t)fun_id;
/* We don't attempt to engage corresponding endpoints now.
* That's the job for network_engage_endpoints() later. */
return 0;
}
int kr_bindings_net(lua_State *L)
{
static const luaL_Reg lib[] = {
{ "list", net_list },
{ "listen", net_listen },
{ "proxy_allowed", net_proxy_allowed },
{ "close", net_close },
{ "interfaces", net_interfaces },
{ "bufsize", net_bufsize },
{ "tcp_pipeline", net_pipeline },
{ "tls", net_tls },
{ "tls_server", net_tls },
{ "tls_client", net_tls_client },
{ "tls_client_clear", net_tls_client_clear },
{ "tls_padding", net_tls_padding },
{ "tls_sticket_secret", net_tls_sticket_secret_string },
{ "tls_sticket_secret_file", net_tls_sticket_secret_file },
{ "outgoing_v4", net_outgoing_v4 },
{ "outgoing_v6", net_outgoing_v6 },
{ "tcp_in_idle", net_tcp_in_idle },
{ "tls_handshake_timeout", net_tls_handshake_timeout },
{ "bpf_set", net_bpf_set },
{ "bpf_clear", net_bpf_clear },
{ "register_endpoint_kind", net_register_endpoint_kind },
{ "doh_headers", net_doh_headers },
{ NULL, NULL }
};
luaL_register(L, "net", lib);
return 1;
}