summaryrefslogtreecommitdiffstats
path: root/daemon/bindings/net.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--daemon/bindings/net.c1260
1 files changed, 1260 insertions, 0 deletions
diff --git a/daemon/bindings/net.c b/daemon/bindings/net.c
new file mode 100644
index 0000000..f1fa6f3
--- /dev/null
+++ b/daemon/bindings/net.c
@@ -0,0 +1,1260 @@
+/* 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) {
+ 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,
+ sizeof(newcfg->ca_files.at[0]), 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,
+ sizeof(newcfg->pins.at[0]), 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);
+ }
+ fclose(fp);
+
+ 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;
+}
+