Commit a05fc84b authored by Tomas Krizek's avatar Tomas Krizek

Merge branch 'http-sockets' into 'master'

http sockets

Closes #85

See merge request !803
parents 380b44f2 ae290f38
......@@ -87,7 +87,7 @@ kres-gen:
- docker
script:
- meson build_ci_lib --prefix=$PREFIX
- ninja -C build_ci_lib lib/libkres.so.${LIBKRES_ABI}
- ninja -C build_ci_lib daemon/kresd
- ninja -C build_ci_lib kres-gen
- git diff --quiet || (git diff; exit 1)
# }}}
......
......@@ -15,6 +15,7 @@ Incompatible changes
- upstream packages for Debian now require systemd
- libknot >= 2.8 is required
- net.list() output format changed
- net.listen() reports error when address-port pair is in use
Improvements
------------
......
......@@ -102,3 +102,15 @@ static inline int execute_callback(lua_State *L, int argc)
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 *));
assert(addr);
memcpy(addr, &p, sizeof(void *));
}
......@@ -23,7 +23,7 @@
#include <stdlib.h>
/** Append 'addr = {port = int, udp = bool, tcp = bool}' */
/** Table and next index on top of stack -> append entries for given endpoint_array_t. */
static int net_list_add(const char *key, void *val, void *ext)
{
lua_State *L = (lua_State *)ext;
......@@ -33,42 +33,58 @@ static int net_list_add(const char *key, void *val, void *ext)
struct endpoint *ep = &ep_array->at[j];
lua_newtable(L); // connection tuple
lua_pushstring(L, key);
lua_setfield(L, -2, "ip");
if (ep->flags.kind) {
lua_pushstring(L, ep->flags.kind);
} else if (ep->flags.tls) {
lua_pushliteral(L, "tls");
} else {
lua_pushliteral(L, "dns");
}
lua_setfield(L, -2, "kind");
lua_newtable(L); // "transport" table
switch (ep->flags.sock_type) {
case SOCK_DGRAM:
lua_pushliteral(L, "udp");
lua_setfield(L, -2, "protocol");
lua_pushinteger(L, ep->port);
lua_setfield(L, -2, "port");
lua_pushliteral(L, "none");
lua_setfield(L, -2, "security");
switch (ep->family) {
case AF_INET:
lua_pushliteral(L, "inet4");
break;
case SOCK_STREAM:
lua_pushliteral(L, "tcp");
lua_setfield(L, -2, "protocol");
lua_pushinteger(L, ep->port);
lua_setfield(L, -2, "port");
if (ep->flags.tls) {
lua_pushliteral(L, "tls");
} else {
lua_pushliteral(L, "none");
}
lua_setfield(L, -2, "security");
case AF_INET6:
lua_pushliteral(L, "inet6");
break;
case AF_UNIX:
lua_pushliteral(L, "unix");
break;
default:
lua_pushliteral(L, "invalid");
assert(!EINVAL);
lua_pushliteral(L, "unknown");
lua_setfield(L, -2, "protocol");
}
lua_setfield(L, -2, "transport");
lua_setfield(L, -2, "family");
lua_newtable(L); // "application" table
lua_pushliteral(L, "dns");
lua_pushstring(L, key);
if (ep->family != AF_UNIX) {
lua_setfield(L, -2, "ip");
} else {
lua_setfield(L, -2, "path");
}
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 {
assert(!EINVAL);
lua_pushliteral(L, "invalid");
}
lua_setfield(L, -2, "protocol");
lua_setfield(L, -2, "application");
lua_setfield(L, -2, "transport");
lua_settable(L, -3);
i++;
......@@ -88,8 +104,10 @@ static int net_list(lua_State *L)
return 1;
}
/** Listen on an address list represented by the top of lua stack. */
static int net_listen_addrs(lua_State *L, int port, bool tls)
/** Listen on an address list represented by the top of lua stack.
* \note kind ownership is not transferred
* \return success */
static bool net_listen_addrs(lua_State *L, int port, bool tls, const char *kind)
{
/* Case: table with 'addr' field; only follow that field directly. */
lua_getfield(L, -1, "addr");
......@@ -103,19 +121,25 @@ static int net_listen_addrs(lua_State *L, int port, bool tls)
const char *str = lua_tostring(L, -1);
if (str != NULL) {
struct engine *engine = engine_luaget(L);
endpoint_flags_t flags = { .tls = tls };
int ret = 0;
if (!tls) {
endpoint_flags_t flags = { .tls = tls };
if (!kind && !flags.tls) { /* normal UDP */
flags.sock_type = SOCK_DGRAM;
ret = network_listen(&engine->net, str, port, flags);
}
if (ret == 0) { /* common for TCP and TLS */
if (!kind && ret == 0) { /* common for normal TCP and TLS */
flags.sock_type = SOCK_STREAM;
ret = network_listen(&engine->net, str, port, flags);
}
if (kind) {
flags.kind = strdup(kind);
flags.sock_type = SOCK_STREAM; /* TODO: allow to override this? */
ret = network_listen(&engine->net, str, port, flags);
}
if (ret != 0) {
kr_log_info("[system] bind to '%s@%d' %s\n",
str, port, kr_strerror(ret));
const char *stype = flags.sock_type == SOCK_DGRAM ? "UDP" : "TCP";
kr_log_error("[system] bind to '%s@%d' (%s): %s\n",
str, port, stype, kr_strerror(ret));
}
return ret == 0;
}
......@@ -125,11 +149,11 @@ static int net_listen_addrs(lua_State *L, int port, bool tls)
lua_error_p(L, "bad type for address");
lua_pushnil(L);
while (lua_next(L, -2)) {
if (net_listen_addrs(L, port, tls) == 0)
return 0;
if (!net_listen_addrs(L, port, tls, kind))
return false;
lua_pop(L, 1);
}
return 1;
return true;
}
static bool table_get_flag(lua_State *L, int index, const char *key, bool def)
......@@ -155,52 +179,72 @@ static int net_listen(lua_State *L)
}
int port = KR_DNS_PORT;
if (n > 1 && lua_isnumber(L, 2)) {
port = lua_tointeger(L, 2);
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)");
}
}
bool tls = (port == KR_DNS_TLS_PORT);
if (n > 2 && lua_istable(L, 3)) {
const char *kind = NULL;
if (n > 2 && !lua_isnil(L, 3)) {
if (!lua_istable(L, 3))
lua_error_p(L, "wrong type of third parameter (table expected)");
tls = table_get_flag(L, 3, "tls", tls);
lua_getfield(L, 3, "kind");
const char *k = lua_tostring(L, -1);
if (k && strcasecmp(k, "dns") == 0) {
tls = false;
} else
if (k && strcasecmp(k, "tls") == 0) {
tls = true;
} else
if (k) {
kind = k;
}
}
/* 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 = kind ? strlen(kind) + 1 : 1 /* 0 length isn't C standard */;
char kind_buf[kind_alen];
if (kind) {
memcpy(kind_buf, kind, kind_alen);
kind = kind_buf;
}
/* Now focus on the first argument. */
lua_pop(L, n - 1);
int res = net_listen_addrs(L, port, tls);
lua_settop(L, 1);
const bool res = net_listen_addrs(L, port, tls, kind);
lua_pushboolean(L, res);
return res;
return 1;
}
/** Close endpoint. */
static int net_close(lua_State *L)
{
/* Check parameters */
int n = lua_gettop(L);
if (n < 2)
lua_error_p(L, "expected 'close(string addr, number port)'");
/* Open resolution context cache */
struct network *net = &engine_luaget(L)->net;
const int n = lua_gettop(L);
bool ok = (n == 1 || n == 2) && lua_isstring(L, 1);
const char *addr = lua_tostring(L, 1);
const uint16_t port = lua_tointeger(L, 2);
endpoint_flags_t flags_all[] = {
{ .sock_type = SOCK_DGRAM, .tls = false },
{ .sock_type = SOCK_STREAM, .tls = false },
{ .sock_type = SOCK_STREAM, .tls = true },
};
bool success = false; /*< at least one deletion succeeded */
int ret = 0;
for (int i = 0; i < sizeof(flags_all) / sizeof(flags_all[0]); ++i) {
ret = network_close(net, addr, port, flags_all[i]);
if (ret == 0) {
success = true;
} else if (ret != kr_error(ENOENT)) {
break;
}
ret = 0;
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;
}
/* true: no fatal error and at least one kr_ok() */
lua_pushboolean(L, ret == 0 && success);
if (!ok)
lua_error_p(L, "expected 'close(string addr, [number port])'");
struct network *net = &engine_luaget(L)->net;
int ret = network_close(net, addr, port);
lua_pushboolean(L, ret == 0);
return 1;
}
......@@ -921,6 +965,47 @@ static int net_bpf_clear(lua_State *L)
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);
struct network *net = &engine_luaget(L)->net;
/* Unregistering */
if (param_count == 1) {
void *val;
if (trie_del(net->endpoint_kinds, kind, kind_len, &val) == KNOT_EOK) {
const int fun_id = (char *)val - (char *)NULL;
luaL_unref(L, LUA_REGISTRYINDEX, fun_id);
return 0;
}
lua_error_p(L, "attempt to unregister unknown kind '%s'\n", kind);
} /* else */
/* Registering */
assert(param_count == 2);
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(net->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 = (char *)NULL + fun_id;
/* We don't attempt to engage correspoinding 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[] = {
......@@ -943,6 +1028,7 @@ int kr_bindings_net(lua_State *L)
{ "tls_handshake_timeout", net_tls_handshake_timeout },
{ "bpf_set", net_bpf_set },
{ "bpf_clear", net_bpf_clear },
{ "register_endpoint_kind", net_register_endpoint_kind },
{ NULL, NULL }
};
register_lib(L, "net", lib);
......
......@@ -60,6 +60,8 @@ machine.
ListenDatagram=[::1]:53000
ListenStream=[::1]:53000
.. _kresd-tls-socket-override-port:
The ``kresd-tls.socket`` can also be configured in the same way to listen for
TLS connections.
......@@ -104,15 +106,17 @@ configured in the config file.
Enable/disable using IPv4 for contacting upstream nameservers.
.. function:: net.listen(addresses, [port = 53, flags = {tls = (port == 853)}])
.. function:: net.listen(addresses, [port = 53, { kind = 'dns' }])
:return: boolean
Listen on addresses; port and flags are optional.
The addresses can be specified as a string or device,
or a list of addresses (recursively).
The command can be given multiple times, but note that it silently skips
any addresses that have already been bound.
The command can be given multiple times,
but repeating an address-port combination is an error.
If you specify port 853, ``kind = 'tls'`` by default.
Examples:
......@@ -120,13 +124,14 @@ configured in the config file.
net.listen('::1')
net.listen(net.lo, 5353)
net.listen({net.eth0, '127.0.0.1'}, 53853, {tls = true})
net.listen({net.eth0, '127.0.0.1'}, 53853, { kind = 'tls' })
net.listen('::', 8453, { kind = 'webmgmt' }) -- see http module
.. function:: net.close(address, [port = 53])
.. function:: net.close(address, [port])
:return: boolean
:return: boolean (at least one endpoint closed)
Close opened address/port pair, noop if not listening.
Close all endpoints listening on the specified address, optionally restricted by port as well.
.. function:: net.list()
......@@ -136,11 +141,33 @@ configured in the config file.
.. code-block:: none
[127.0.0.1] => {
[port] => 53
[tcp] => true
[udp] => true
}
[1] => {
[kind] => tls
[transport] => {
[family] => inet4
[ip] => 127.0.0.1
[port] => 853
[protocol] => tcp
}
}
[2] => {
[kind] => dns
[transport] => {
[family] => inet6
[ip] => ::1
[port] => 53
[protocol] => udp
}
}
[3] => {
[kind] => dns
[transport] => {
[family] => inet6
[ip] => ::1
[port] => 53
[protocol] => tcp
}
}
.. function:: net.interfaces()
......
......@@ -680,10 +680,19 @@ void engine_deinit(struct engine *engine)
if (engine == NULL) {
return;
}
/* Only close sockets and services; no need to clean up mempool. */
/* Only close sockets and services,
* no need to clean up mempool. */
network_deinit(&engine->net);
/* Network deinit is split up. We first need to stop listening,
* then we can unload modules during which we still want
* e.g. the endpoint kind registry to work (inside ->net),
* and this registry deinitization uses the lua state. */
network_close_force(&engine->net);
for (size_t i = 0; i < engine->ipc_set.len; ++i) {
close(engine->ipc_set.at[i]);
}
for (size_t i = 0; i < engine->modules.len; ++i) {
engine_unload(engine, engine->modules.at[i]);
}
kr_zonecut_deinit(&engine->resolver.root_hints);
kr_cache_close(&engine->resolver.cache);
......@@ -692,18 +701,8 @@ void engine_deinit(struct engine *engine)
lru_free(engine->resolver.cache_rep);
lru_free(engine->resolver.cache_cookie);
/* Clear IPC pipes */
for (size_t i = 0; i < engine->ipc_set.len; ++i) {
close(engine->ipc_set.at[i]);
}
/* Unload modules and engine. */
for (size_t i = 0; i < engine->modules.len; ++i) {
engine_unload(engine, engine->modules.at[i]);
}
if (engine->L) {
lua_close(engine->L);
}
network_deinit(&engine->net);
lua_close(engine->L);
/* Free data structures */
array_clear(engine->modules);
......
......@@ -104,40 +104,48 @@ void udp_recv(uv_udp_t *handle, ssize_t nread, const uv_buf_t *buf,
mp_flush(worker->pkt_pool.ctx);
}
static int udp_bind_finalize(uv_handle_t *handle)
int io_bind(const struct sockaddr *addr, int type)
{
check_bufsize(handle);
/* Handle is already created, just create context. */
struct session *s = session_new(handle, false);
assert(s);
session_flags(s)->outgoing = false;
return io_start_read(handle);
}
const int fd = socket(addr->sa_family, type, 0);
if (fd < 0) return kr_error(errno);
int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)))
return kr_error(errno);
#ifdef SO_REUSEPORT
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)))
return kr_error(errno);
#endif
#ifdef IPV6_V6ONLY
if (addr->sa_family == AF_INET6
&& setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)))
return kr_error(errno);
#endif
int udp_bind(uv_udp_t *handle, const struct sockaddr *addr)
{
unsigned flags = UV_UDP_REUSEADDR;
if (addr->sa_family == AF_INET6) {
flags |= UV_UDP_IPV6ONLY;
}
int ret = uv_udp_bind(handle, addr, flags);
if (ret != 0) {
return ret;
}
return udp_bind_finalize((uv_handle_t *)handle);
if (bind(fd, addr, kr_sockaddr_len(addr)))
return kr_error(errno);
return fd;
}
int udp_bindfd(uv_udp_t *handle, int fd)
int io_listen_udp(uv_loop_t *loop, uv_udp_t *handle, int fd)
{
if (!handle) {
return kr_error(EINVAL);
}
int ret = uv_udp_init(loop, handle);
if (ret) return ret;
int ret = uv_udp_open(handle, (uv_os_sock_t) fd);
if (ret != 0) {
return ret;
}
return udp_bind_finalize((uv_handle_t *)handle);
ret = uv_udp_open(handle, fd);
if (ret) return ret;
uv_handle_t *h = (uv_handle_t *)handle;
check_bufsize(h);
/* Handle is already created, just create context. */
struct session *s = session_new(h, false);
assert(s);
session_flags(s)->outgoing = false;
return io_start_read(h);
}
void tcp_timeout_trigger(uv_timer_t *timer)
......@@ -348,47 +356,24 @@ static void tls_accept(uv_stream_t *master, int status)
_tcp_accept(master, status, true);
}
static int set_tcp_option(uv_handle_t *handle, int option, int val)
int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bool has_tls)
{
uv_os_fd_t fd = 0;
if (uv_fileno(handle, &fd) == 0) {
return setsockopt(fd, IPPROTO_TCP, option, &val, sizeof(val));
const uv_connection_cb connection = has_tls ? tls_accept : tcp_accept;
if (!handle) {
return kr_error(EINVAL);
}
return 0; /* N/A */
}
int ret = uv_tcp_init(loop, handle);
if (ret) return ret;
static int tcp_bind_finalize(uv_handle_t *handle)
{
/* TCP_FASTOPEN enables 1 RTT connection resumptions. */
#ifdef TCP_FASTOPEN
# ifdef __linux__
(void) set_tcp_option(handle, TCP_FASTOPEN, 16); /* Accepts queue length hint */
# else
(void) set_tcp_option(handle, TCP_FASTOPEN, 1); /* Accepts on/off */
# endif
#endif
handle->data = NULL;
return 0;
}
static int _tcp_bind(uv_tcp_t *handle, const struct sockaddr *addr,
uv_connection_cb connection, int tcp_backlog)
{
unsigned flags = 0;
if (addr->sa_family == AF_INET6) {
flags |= UV_TCP_IPV6ONLY;
}
int ret = uv_tcp_bind(handle, addr, flags);
if (ret != 0) {
return ret;
}
ret = uv_tcp_open(handle, (uv_os_sock_t) fd);
if (ret) return ret;
int val; (void)val;
/* TCP_DEFER_ACCEPT delays accepting connections until there is readable data. */
#ifdef TCP_DEFER_ACCEPT
if (set_tcp_option((uv_handle_t *)handle, TCP_DEFER_ACCEPT, KR_CONN_RTT_MAX/1000) != 0) {
kr_log_info("[ io ] tcp_bind (defer_accept): %s\n", strerror(errno));
val = KR_CONN_RTT_MAX/1000;
if (setsockopt(fd, IPPROTO_TCP, TCP_DEFER_ACCEPT, &val, sizeof(val))) {
kr_log_error("[ io ] tcp_bind (defer_accept): %s\n", strerror(errno));
}
#endif
......@@ -397,45 +382,20 @@ static int _tcp_bind(uv_tcp_t *handle, const struct sockaddr *addr,
return ret;
}
return tcp_bind_finalize((uv_handle_t *)handle);
}
int tcp_bind(uv_tcp_t *handle, const struct sockaddr *addr, int tcp_backlog)
{
return _tcp_bind(handle, addr, tcp_accept, tcp_backlog);
}
int tcp_bind_tls(uv_tcp_t *handle, const struct sockaddr *addr, int tcp_backlog)
{
return _tcp_bind(handle, addr, tls_accept, tcp_backlog);
}
static int _tcp_bindfd(uv_tcp_t *handle, int fd, uv_connection_cb connection, int tcp_backlog)
{
if (!handle) {
return kr_error(EINVAL);
}
int ret = uv_tcp_open(handle, (uv_os_sock_t) fd);
if (ret != 0) {
return ret;
}
ret = uv_listen((uv_stream_t *)handle, tcp_backlog, connection);
if (ret != 0) {
return ret;
/* TCP_FASTOPEN enables 1 RTT connection resumptions. */
#ifdef TCP_FASTOPEN
#ifdef __linux__
val = 16; /* Accepts queue length hint */
#else
val = 1; /* Accepts on/off */
#endif
if (setsockopt(fd, IPPROTO_TCP, TCP_FASTOPEN, &val, sizeof(val))) {
kr_log_error("[ io ] tcp_bind (fastopen): %s\n", strerror(errno));
}
return tcp_bind_finalize((uv_handle_t *)handle);
}
int tcp_bindfd(uv_tcp_t *handle, int fd, int tcp_backlog)
{
return _tcp_bindfd(handle, fd, tcp_accept, tcp_backlog);
}
#endif
int tcp_bindfd_tls(uv_tcp_t *handle, int fd, int tcp_backlog)
{
return _tcp_bindfd(handle, fd, tls_accept, tcp_backlog);
handle->data = NULL;
return 0;
}
int io_create(uv_loop_t *loop, uv_handle_t *handle, int type, unsigned family, bool has_tls)
......
......@@ -25,12 +25,13 @@
struct tls_ctx_t;
struct tls_client_ctx_t;
int udp_bind(uv_udp_t *handle, const struct sockaddr *addr);
int udp_bindfd(uv_udp_t *handle, int fd);
int tcp_bind(uv_tcp_t *handle, const struct sockaddr *addr, int tcp_backlog);
int tcp_bind_tls(uv_tcp_t *handle, const struct sockaddr *addr, int tcp_backlog);
int tcp_bindfd(uv_tcp_t *handle, int fd, int tcp_backlog);
int tcp_bindfd_tls(uv_tcp_t *handle, int fd, int tcp_backlog);
/** Bind address into a file-descriptor (only, no libuv). type is e.g. SOCK_DGRAM */
int io_bind(const struct sockaddr *addr, int type);
/** Initialize a UDP handle and start listening. */
int io_listen_udp(uv_loop_t *loop, uv_udp_t *handle, int fd);
/** Initialize a TCP handle and start listening. */
int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bool has_tls);
void tcp_timeout_trigger(uv_timer_t *timer);
/** Initialize the handle, incl. ->data = struct session * instance.
......
......@@ -348,6 +348,19 @@ int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
int kr_cache_commit(struct kr_cache *);
uint32_t packet_ttl(const knot_pkt_t *, _Bool);
typedef struct {
int sock_type;
_Bool tls;
const char *kind;
} endpoint_flags_t;
struct endpoint {
void *handle;
int fd;
int family;
uint16_t port;
_Bool engaged;
endpoint_flags_t flags;
};
typedef struct {
uint8_t bitmap[32];
uint8_t length;
......
......@@ -5,6 +5,7 @@ set -o pipefail -o errexit -o nounset
cd "$(dirname ${0})"
CDEFS="../../scripts/gen-cdefs.sh"
LIBKRES="${MESON_BUILD_ROOT}/lib/libkres.so"
KRESD="${MESON_BUILD_ROOT}/daemon/kresd"
# Write to kres-gen.lua instead of stdout
mv kres-gen.lua{,.bak} ||:
......@@ -215,6 +216,11 @@ ${CDEFS} ${LIBKRES} functions <<-EOF
packet_ttl
EOF