1
0
Fork 0
knot-resolver/daemon/bindings/net.c
Daniel Baumann fbc604e215
Adding upstream version 5.7.5.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-21 13:56:17 +02:00

1264 lines
37 KiB
C

/* 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_worker->engine->net.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) {
struct network *net = &the_worker->engine->net;
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(net, 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(net, 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(net, 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)
{
struct network *net = &the_worker->engine->net;
int n = lua_gettop(L);
int i = 1;
const char *addr;
/* Return current state */
if (n == 0) {
lua_newtable(L);
i = 1;
if (net->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, net->proxy_addrs4, &i);
}
if (net->proxy_all6) {
lua_pushinteger(L, i);
lua_pushstring(L, "::/0");
lua_settable(L, -3);
i += 1;
} else {
net_proxy_addr_put(L, AF_INET6, net->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(net);
/* 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(net, 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(net, 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(&the_worker->engine->net, 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)
{
struct kr_context *ctx = &the_worker->engine->resolver;
const int argc = lua_gettop(L);
if (argc == 0) {
lua_pushinteger(L, knot_edns_get_payload(ctx->downstream_opt_rr));
lua_pushinteger(L, knot_edns_get_payload(ctx->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(ctx->downstream_opt_rr, (uint16_t)bufsize);
knot_edns_set_payload(ctx->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(ctx->downstream_opt_rr, (uint16_t)bufsize_downstream);
knot_edns_set_payload(ctx->upstream_opt_rr, (uint16_t)bufsize_upstream);
}
return 0;
}
/** Set TCP pipelining size. */
static int net_pipeline(lua_State *L)
{
struct worker_ctx *worker = the_worker;
if (!worker) {
return 0;
}
if (!lua_isnumber(L, 1)) {
lua_pushinteger(L, 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) ">");
worker->tcp_pipeline_max = len;
lua_pushinteger(L, len);
return 1;
}
static int net_tls(lua_State *L)
{
struct network *net = &the_worker->engine->net;
if (!net) {
return 0;
}
/* Only return current credentials. */
if (lua_gettop(L) == 0) {
/* No credentials configured yet. */
if (!net->tls_credentials) {
return 0;
}
lua_newtable(L);
lua_pushstring(L, net->tls_credentials->tls_cert);
lua_setfield(L, -2, "cert_file");
lua_pushstring(L, net->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(net, 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.
*/
struct network *net = &the_worker->engine->net;
if (lua_gettop(L) == 0)
return tls_params2lua(L, net->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", 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(
&net->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. */
struct network *net = &the_worker->engine->net;
int r = tls_client_param_remove(net->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)
{
struct kr_context *ctx = &the_worker->engine->resolver;
/* Only return current padding. */
if (lua_gettop(L) == 0) {
if (ctx->tls_padding < 0) {
lua_pushboolean(L, true);
return 1;
} else if (ctx->tls_padding == 0) {
lua_pushboolean(L, false);
return 1;
}
lua_pushinteger(L, ctx->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) {
ctx->tls_padding = -1;
} else {
ctx->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);
ctx->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)
{
struct network *net = &the_worker->engine->net;
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(net->tls_session_ticket_ctx);
net->tls_session_ticket_ctx =
tls_session_ticket_ctx_create(net->loop, secret, secret_len);
if (net->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);
}
struct network *net = &the_worker->engine->net;
tls_session_ticket_ctx_destroy(net->tls_session_ticket_ctx);
net->tls_session_ticket_ctx =
tls_session_ticket_ctx_create(net->loop, secret_buf, secret_len);
if (net->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)
{
struct network *net = &the_worker->engine->net;
return net_update_timeout(L, &net->tcp.in_idle_timeout, "net.tcp_in_idle");
}
static int net_tls_handshake_timeout(lua_State *L)
{
struct network *net = &the_worker->engine->net;
return net_update_timeout(L, &net->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(&the_worker->engine->net, 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(&the_worker->engine->net);
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);
struct network *net = &the_worker->engine->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 -> 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(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 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;
}