From: Jan Hak Date: Fri, 13 Mar 2020 14:02:50 +0000 (+0100) Subject: doh: C implementation of DoH - WiP X-Git-Tag: v5.2.0~15^2~52 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b0c50d484fce3604f746c3f93c54fdb411be9eb9;p=thirdparty%2Fknot-resolver.git doh: C implementation of DoH - WiP Working server-side GET/POST HTTPS method - Proof-of-Concept Working server-side GET/POST HTTP/2 method - WiP --- diff --git a/contrib/base64url.c b/contrib/base64url.c new file mode 100644 index 000000000..05697f45e --- /dev/null +++ b/contrib/base64url.c @@ -0,0 +1,268 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include "contrib/base64url.h" +#include "libknot/errcode.h" + +#include +#include + +/*! \brief Maximal length of binary input to Base64url encoding. */ +#define MAX_BIN_DATA_LEN ((INT32_MAX / 4) * 3) + +/*! \brief Base64url padding character. */ +static const uint8_t base64url_pad = '\0'; +/*! \brief Base64 alphabet. */ +static const uint8_t base64url_enc[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + +/*! \brief Indicates bad Base64 character. */ +#define KO 255 +/*! \brief Indicates Base64 padding character. */ +#define PD 64 + +/*! \brief Transformation and validation table for decoding Base64. */ +static const uint8_t base64url_dec[256] = { + [ 0] = PD, [ 43] = KO, ['V'] = 21, [129] = KO, [172] = KO, [215] = KO, + [ 1] = KO, [ 44] = KO, ['W'] = 22, [130] = KO, [173] = KO, [216] = KO, + [ 2] = KO, ['-'] = 62, ['X'] = 23, [131] = KO, [174] = KO, [217] = KO, + [ 3] = KO, [ 46] = KO, ['Y'] = 24, [132] = KO, [175] = KO, [218] = KO, + [ 4] = KO, [ 47] = KO, ['Z'] = 25, [133] = KO, [176] = KO, [219] = KO, + [ 5] = KO, ['0'] = 52, [ 91] = KO, [134] = KO, [177] = KO, [220] = KO, + [ 6] = KO, ['1'] = 53, [ 92] = KO, [135] = KO, [178] = KO, [221] = KO, + [ 7] = KO, ['2'] = 54, [ 93] = KO, [136] = KO, [179] = KO, [222] = KO, + [ 8] = KO, ['3'] = 55, [ 94] = KO, [137] = KO, [180] = KO, [223] = KO, + [ 9] = KO, ['4'] = 56, ['_'] = 63, [138] = KO, [181] = KO, [224] = KO, + [ 10] = KO, ['5'] = 57, [ 96] = KO, [139] = KO, [182] = KO, [225] = KO, + [ 11] = KO, ['6'] = 58, ['a'] = 26, [140] = KO, [183] = KO, [226] = KO, + [ 12] = KO, ['7'] = 59, ['b'] = 27, [141] = KO, [184] = KO, [227] = KO, + [ 13] = KO, ['8'] = 60, ['c'] = 28, [142] = KO, [185] = KO, [228] = KO, + [ 14] = KO, ['9'] = 61, ['d'] = 29, [143] = KO, [186] = KO, [229] = KO, + [ 15] = KO, [ 58] = KO, ['e'] = 30, [144] = KO, [187] = KO, [230] = KO, + [ 16] = KO, [ 59] = KO, ['f'] = 31, [145] = KO, [188] = KO, [231] = KO, + [ 17] = KO, [ 60] = KO, ['g'] = 32, [146] = KO, [189] = KO, [232] = KO, + [ 18] = KO, [ 61] = KO, ['h'] = 33, [147] = KO, [190] = KO, [233] = KO, + [ 19] = KO, [ 62] = KO, ['i'] = 34, [148] = KO, [191] = KO, [234] = KO, + [ 20] = KO, [ 63] = KO, ['j'] = 35, [149] = KO, [192] = KO, [235] = KO, + [ 21] = KO, [ 64] = KO, ['k'] = 36, [150] = KO, [193] = KO, [236] = KO, + [ 22] = KO, ['A'] = 0, ['l'] = 37, [151] = KO, [194] = KO, [237] = KO, + [ 23] = KO, ['B'] = 1, ['m'] = 38, [152] = KO, [195] = KO, [238] = KO, + [ 24] = KO, ['C'] = 2, ['n'] = 39, [153] = KO, [196] = KO, [239] = KO, + [ 25] = KO, ['D'] = 3, ['o'] = 40, [154] = KO, [197] = KO, [240] = KO, + [ 26] = KO, ['E'] = 4, ['p'] = 41, [155] = KO, [198] = KO, [241] = KO, + [ 27] = KO, ['F'] = 5, ['q'] = 42, [156] = KO, [199] = KO, [242] = KO, + [ 28] = KO, ['G'] = 6, ['r'] = 43, [157] = KO, [200] = KO, [243] = KO, + [ 29] = KO, ['H'] = 7, ['s'] = 44, [158] = KO, [201] = KO, [244] = KO, + [ 30] = KO, ['I'] = 8, ['t'] = 45, [159] = KO, [202] = KO, [245] = KO, + [ 31] = KO, ['J'] = 9, ['u'] = 46, [160] = KO, [203] = KO, [246] = KO, + [ 32] = KO, ['K'] = 10, ['v'] = 47, [161] = KO, [204] = KO, [247] = KO, + [ 33] = KO, ['L'] = 11, ['w'] = 48, [162] = KO, [205] = KO, [248] = KO, + [ 34] = KO, ['M'] = 12, ['x'] = 49, [163] = KO, [206] = KO, [249] = KO, + [ 35] = KO, ['N'] = 13, ['y'] = 50, [164] = KO, [207] = KO, [250] = KO, + [ 36] = KO, ['O'] = 14, ['z'] = 51, [165] = KO, [208] = KO, [251] = KO, + [ 37] = KO, ['P'] = 15, [123] = KO, [166] = KO, [209] = KO, [252] = KO, + [ 38] = KO, ['Q'] = 16, [124] = KO, [167] = KO, [210] = KO, [253] = KO, + [ 39] = KO, ['R'] = 17, [125] = KO, [168] = KO, [211] = KO, [254] = KO, + [ 40] = KO, ['S'] = 18, [126] = KO, [169] = KO, [212] = KO, [255] = KO, + [ 41] = KO, ['T'] = 19, [127] = KO, [170] = KO, [213] = KO, + [ 42] = KO, ['U'] = 20, [128] = KO, [171] = KO, [214] = KO, +}; + +int32_t kr_base64url_encode(const uint8_t *in, + const uint32_t in_len, + uint8_t *out, + const uint32_t out_len) +{ + // Checking inputs. + if (in == NULL || out == NULL) { + return KNOT_EINVAL; + } + if (in_len > MAX_BIN_DATA_LEN || out_len < ((in_len + 2) / 3) * 4) { + return KNOT_ERANGE; + } + + uint8_t rest_len = in_len % 3; + const uint8_t *stop = in + in_len - rest_len; + uint8_t *text = out; + + // Encoding loop takes 3 bytes and creates 4 characters. + while (in < stop) { + text[0] = base64url_enc[in[0] >> 2]; + text[1] = base64url_enc[(in[0] & 0x03) << 4 | in[1] >> 4]; + text[2] = base64url_enc[(in[1] & 0x0F) << 2 | in[2] >> 6]; + text[3] = base64url_enc[in[2] & 0x3F]; + text += 4; + in += 3; + } + + // Processing of padding, if any. + switch (rest_len) { + case 2: + text[0] = base64url_enc[in[0] >> 2]; + text[1] = base64url_enc[(in[0] & 0x03) << 4 | in[1] >> 4]; + text[2] = base64url_enc[(in[1] & 0x0F) << 2]; + text[3] = base64url_pad; + text += 3; + break; + case 1: + text[0] = base64url_enc[in[0] >> 2]; + text[1] = base64url_enc[(in[0] & 0x03) << 4]; + text[2] = base64url_pad; + text[3] = base64url_pad; + text += 2; + break; + } + return (text - out); +} + +int32_t kr_base64url_encode_alloc(const uint8_t *in, + const uint32_t in_len, + uint8_t **out) +{ + // Checking inputs. + if (out == NULL) { + return KNOT_EINVAL; + } + if (in_len > MAX_BIN_DATA_LEN) { + return KNOT_ERANGE; + } + + // Compute output buffer length. + uint32_t out_len = ((in_len + 2) / 3) * 4; + + // Allocate output buffer. + *out = malloc(out_len); + if (*out == NULL) { + return KNOT_ENOMEM; + } + + // Encode data. + int32_t ret = kr_base64url_encode(in, in_len, *out, out_len); + if (ret < 0) { + free(*out); + *out = NULL; + } + + return ret; +} + +int32_t kr_base64url_decode(const uint8_t *in, + const uint32_t in_len, + uint8_t *out, + const uint32_t out_len) +{ + // Checking inputs. + if (in == NULL || out == NULL) { + return KNOT_EINVAL; + } + if (in_len > INT32_MAX || out_len < ((in_len + 3) / 4) * 3) { + return KNOT_ERANGE; + } + + const uint8_t *stop = in + in_len; + uint8_t *bin = out; + uint8_t pad_len = 0; + uint8_t c1, c2, c3, c4; + + // Decoding loop takes 4 characters and creates 3 bytes. + while (in < stop) { + // Filling and transforming 4 Base64 chars. + c1 = base64url_dec[in[0]] ; + c2 = (in + 1 < stop) ? base64url_dec[in[1]] : PD; + c3 = (in + 2 < stop) ? base64url_dec[in[2]] : PD; + c4 = (in + 3 < stop) ? base64url_dec[in[3]] : PD; + + // Check 4. char if is bad or padding. + if (c4 >= PD) { + if (c4 == PD && pad_len == 0) { + pad_len = 1; + } else { + return KNOT_BASE64_ECHAR; + } + } + + // Check 3. char if is bad or padding. + if (c3 >= PD) { + if (c3 == PD && pad_len == 1) { + pad_len = 2; + } else { + return KNOT_BASE64_ECHAR; + } + } + + // Check 1. and 2. chars if are not padding. + if (c2 >= PD || c1 >= PD) { + return KNOT_BASE64_ECHAR; + } + + // Computing of output data based on padding length. + switch (pad_len) { + case 0: + bin[2] = (c3 << 6) + c4; + // FALLTHROUGH + case 1: + bin[1] = (c2 << 4) + (c3 >> 2); + // FALLTHROUGH + case 2: + bin[0] = (c1 << 2) + (c2 >> 4); + } + + // Update output end. + switch (pad_len) { + case 0: + bin += 3; + break; + case 1: + bin += 2; + break; + case 2: + bin += 1; + break; + } + + in += 4; + } + + return (bin - out); +} + +int32_t kr_base64url_decode_alloc(const uint8_t *in, + const uint32_t in_len, + uint8_t **out) +{ + // Checking inputs. + if (out == NULL) { + return KNOT_EINVAL; + } + + // Compute output buffer length. + uint32_t out_len = ((in_len + 3) / 4) * 3; + + // Allocate output buffer. + *out = malloc(out_len); + if (*out == NULL) { + return KNOT_ENOMEM; + } + + // Decode data. + int32_t ret = kr_base64url_decode(in, in_len, *out, out_len); + if (ret < 0) { + free(*out); + *out = NULL; + } + + return ret; +} diff --git a/contrib/base64url.h b/contrib/base64url.h new file mode 100644 index 000000000..66a3afccc --- /dev/null +++ b/contrib/base64url.h @@ -0,0 +1,103 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/*! + * \brief Base64url implementation (RFC 4648). + */ + +#pragma once + +#include + +/*! + * \brief Encodes binary data using Base64. + * + * \note Output data buffer contains Base64 text string which isn't + * terminated with '\0'! + * + * \param in Input binary data. + * \param in_len Length of input data. + * \param out Output data buffer. + * \param out_len Size of output buffer. + * + * \retval >=0 length of output string. + * \retval KNOT_E* if error. + */ +int32_t kr_base64url_encode(const uint8_t *in, + const uint32_t in_len, + uint8_t *out, + const uint32_t out_len); + +/*! + * \brief Encodes binary data using Base64 and output stores to own buffer. + * + * \note Output data buffer contains Base64 text string which isn't + * terminated with '\0'! + * + * \note Output buffer should be deallocated after use. + * + * \param in Input binary data. + * \param in_len Length of input data. + * \param out Output data buffer. + * + * \retval >=0 length of output string. + * \retval KNOT_E* if error. + */ +int32_t kr_base64url_encode_alloc(const uint8_t *in, + const uint32_t in_len, + uint8_t **out); + +/*! + * \brief Decodes text data using Base64. + * + * \note Input data needn't be terminated with '\0'. + * + * \note Input data must be continuous Base64 string! + * + * \param in Input text data. + * \param in_len Length of input string. + * \param out Output data buffer. + * \param out_len Size of output buffer. + * + * \retval >=0 length of output data. + * \retval KNOT_E* if error. + */ +int32_t kr_base64url_decode(const uint8_t *in, + const uint32_t in_len, + uint8_t *out, + const uint32_t out_len); + +/*! + * \brief Decodes text data using Base64 and output stores to own buffer. + * + * \note Input data needn't be terminated with '\0'. + * + * \note Input data must be continuous Base64 string! + * + * \note Output buffer should be deallocated after use. + * + * \param in Input text data. + * \param in_len Length of input string. + * \param out Output data buffer. + * + * \retval >=0 length of output data. + * \retval KNOT_E* if error. + */ +int32_t kr_base64url_decode_alloc(const uint8_t *in, + const uint32_t in_len, + uint8_t **out); + +/*! @} */ diff --git a/contrib/meson.build b/contrib/meson.build index 9e6a6390f..effbafda4 100644 --- a/contrib/meson.build +++ b/contrib/meson.build @@ -8,7 +8,8 @@ contrib_src = files([ 'ucw/mempool-fmt.c', 'murmurhash3/murmurhash3.c', 'base32hex.c', - 'base64.c' + 'base64.c', + 'base64url.c' ]) contrib_inc = include_directories('.', '..') diff --git a/daemon/bindings/net.c b/daemon/bindings/net.c index 2131d16cb..1e6a992fb 100644 --- a/daemon/bindings/net.c +++ b/daemon/bindings/net.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2015-2019 CZ.NIC, z.s.p.o. +/* Copyright (C) 2015-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -95,7 +95,7 @@ static int net_list(lua_State *L) /** 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, bool freebind) +static bool net_listen_addrs(lua_State *L, int port, bool tls, bool http, const char *kind, bool freebind) { /* Case: table with 'addr' field; only follow that field directly. */ lua_getfield(L, -1, "addr"); @@ -110,7 +110,7 @@ static bool net_listen_addrs(lua_State *L, int port, bool tls, const char *kind, if (str != NULL) { struct network *net = &the_worker->engine->net; int ret = 0; - endpoint_flags_t flags = { .tls = tls, .freebind = freebind }; + endpoint_flags_t flags = { .tls = tls, .http = http, .freebind = freebind }; if (!kind && !flags.tls) { /* normal UDP */ flags.sock_type = SOCK_DGRAM; ret = network_listen(net, str, port, flags); @@ -142,7 +142,7 @@ static bool net_listen_addrs(lua_State *L, int port, bool tls, const char *kind, lua_error_p(L, "bad type for address"); lua_pushnil(L); while (lua_next(L, -2)) { - if (!net_listen_addrs(L, port, tls, kind, freebind)) + if (!net_listen_addrs(L, port, tls, http, kind, freebind)) return false; lua_pop(L, 1); } @@ -182,6 +182,8 @@ static int net_listen(lua_State *L) } bool tls = (port == KR_DNS_TLS_PORT); + bool http = (port == KR_DNS_HTTP_PORT); + http = tls = (port == KR_DNS_DOH_PORT); bool freebind = false; const char *kind = NULL; if (n > 2 && !lua_isnil(L, 3)) { @@ -193,10 +195,19 @@ static int net_listen(lua_State *L) lua_getfield(L, 3, "kind"); const char *k = lua_tostring(L, -1); if (k && strcasecmp(k, "dns") == 0) { - tls = false; + tls = http = false; } else if (k && strcasecmp(k, "tls") == 0) { tls = true; + http = false; + } else + //TODO temporary moved HTTP (without TLS) here + if (k && strcasecmp(k, "http") == 0) { + tls = false; + http = true; + } else + if (k && strcasecmp(k, "doh") == 0) { + tls = http = true; } else if (k) { kind = k; @@ -214,7 +225,7 @@ static int net_listen(lua_State *L) /* Now focus on the first argument. */ lua_settop(L, 1); - if (!net_listen_addrs(L, port, tls, kind, freebind)) + if (!net_listen_addrs(L, port, tls, http, kind, freebind)) lua_error_p(L, "net.listen() failed to bind"); lua_pushboolean(L, true); return 1; diff --git a/daemon/http.c b/daemon/http.c new file mode 100644 index 000000000..d14555533 --- /dev/null +++ b/daemon/http.c @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 CZ.NIC, z.s.p.o + * + * Initial Author: Jan Hák + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#include +#include +#include +#include +#include + +#include "daemon/io.h" +#include "daemon/http.h" +#include "daemon/worker.h" +#include "daemon/session.h" + +#include "contrib/base64url.h" + +#define MAKE_NV(K, KS, V, VS) \ + { (uint8_t *)K, (uint8_t *)V, KS, VS, NGHTTP2_NV_FLAG_NONE } + +#define MAKE_STATIC_NV(K, V) \ + MAKE_NV(K, sizeof(K) - 1, V, sizeof(V) - 1) + +#define HTTP_MAX_CONCURRENT_STREAMS 1 + +#define MAX_DECIMAL_LENGTH(VT) (CHAR_BIT * sizeof(VT) / 3) + 3 + +struct http_data_buffer { + uint8_t *data; + const uint8_t *end; +}; + +static char const server_logstring[] = "http"; + +static ssize_t send_callback(nghttp2_session *session, const uint8_t *data, size_t length, int flags, void *user_data) +{ + struct http_ctx_t *ctx = (struct http_ctx_t *)user_data; + return ctx->send_cb(data, length, ctx->user_ctx); +} + +static int header_callback(nghttp2_session *session, const nghttp2_frame *frame, const uint8_t *name, size_t namelen, const uint8_t *value, size_t valuelen, uint8_t flags, void *user_data) +{ + //TODO some validation.. When POST, no DNS variable in path... + //In knot we parse path using some static lib, think of use it too but not necessary + static const char key[] = "dns="; + struct http_ctx_t *ctx = (struct http_ctx_t *)user_data; + if (!strcasecmp(":path", (const char *)name)) { + char *beg = strstr((const char *)value, key); + if (beg) { + beg += sizeof(key) - 1; + char *end = strchrnul(beg, '&'); + ctx->wire_len = kr_base64url_decode((uint8_t*)beg, end - beg, ctx->wire + sizeof(uint16_t), ctx->wire_len - sizeof(uint16_t)); + ctx->request_stream_id = frame->hd.stream_id; + } + } + return 0; +} + +static int query_recv_callback(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *user_data) +{ + struct http_ctx_t *ctx = (struct http_ctx_t *)user_data; + memcpy(ctx->wire + sizeof(uint16_t), data, len); + ctx->wire_len = len; + ctx->request_stream_id = stream_id; + return 0; +} + +struct http_ctx_t* http_new(http_send_callback cb, void *user_ctx) +{ + assert(cb != NULL); + + nghttp2_session_callbacks *callbacks; + nghttp2_session_callbacks_new(&callbacks); + nghttp2_session_callbacks_set_send_callback(callbacks, send_callback); + nghttp2_session_callbacks_set_on_header_callback(callbacks, header_callback); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, query_recv_callback); + + struct http_ctx_t *ctx = calloc(1UL, sizeof(struct http_ctx_t)); + ctx->send_cb = cb; + ctx->user_ctx = user_ctx; + ctx->request_stream_id = -1; + + nghttp2_session_server_new(&ctx->session, callbacks, ctx); + nghttp2_session_callbacks_del(callbacks); + + static const nghttp2_settings_entry iv[] = { + { NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, HTTP_MAX_CONCURRENT_STREAMS } + }; + + nghttp2_submit_settings(ctx->session, NGHTTP2_FLAG_NONE, iv, sizeof(iv)/sizeof(*iv) ); + + return ctx; +} + +ssize_t http_process_input_data(struct session *s, const uint8_t *in_buf, ssize_t in_buf_len) +{ + struct http_ctx_t *http_p = session_http_get_server_ctx(s); + if (!http_p->session) { + return kr_error(ENOSYS); + } + + http_p->wire = session_wirebuf_get_free_start(s); + http_p->wire_len = session_wirebuf_get_free_size(s); + ssize_t ret = 0; + if ((ret = nghttp2_session_mem_recv(http_p->session, in_buf, in_buf_len)) < 0) { + kr_log_error("[%s] nghttp2_session_mem_recv failed: %s (%zd)\n", server_logstring, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + if ((ret = nghttp2_session_send(http_p->session)) < 0) { + kr_log_error("[%s] nghttp2_session_send failed: %s (%zd)\n", server_logstring, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + ssize_t submitted = 0; + if (http_p->request_stream_id >= 0) { + knot_wire_write_u16(http_p->wire, http_p->wire_len); + submitted = http_p->wire_len + sizeof(uint16_t); + } + + return submitted; +} + +static ssize_t send_response_callback(nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length, uint32_t *data_flags, nghttp2_data_source *source, void *user_data) +{ + struct http_data_buffer *buffer = (struct http_data_buffer *)source->ptr; + struct http_ctx_t *ctx = (struct http_ctx_t *)user_data; //TODO remove maybe + size_t send = MIN(buffer->end - buffer->data, length); + memcpy(buf, buffer->data, send); + buffer->data += send; + //*data_flags |= (buffer->data == buffer->end) ? NGHTTP2_DATA_FLAG_EOF : 0; + if (buffer->data == buffer->end) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + ctx->request_stream_id = -1; + } + return send; +} + +int http_write(uv_write_t *req, uv_handle_t *handle, knot_pkt_t *pkt, uv_write_cb cb) +{ + if (!pkt || !handle || !handle->data) { + return kr_error(EINVAL); + } + + struct session *s = handle->data; + struct http_ctx_t *http_ctx = session_http_get_server_ctx(s); + + assert (http_ctx); + assert (!session_flags(s)->outgoing); + + char size[MAX_DECIMAL_LENGTH(pkt->size)] = { 0 }; + int size_len = sprintf(size, "%ld", pkt->size); + + struct http_data_buffer data_buff = { + .data = pkt->wire, + .end = pkt->wire + pkt->size + }; + + const nghttp2_data_provider data_prd = { + .source = { + .ptr = &data_buff + }, + .read_callback = send_response_callback + }; + + nghttp2_nv hdrs[] = { + MAKE_STATIC_NV(":status", "200"), + MAKE_STATIC_NV("content-type", "application/dns-message"), + MAKE_NV("content-length", 14, size, size_len) + }; + + int ret = nghttp2_submit_response(http_ctx->session, http_ctx->request_stream_id, hdrs, sizeof(hdrs)/sizeof(*hdrs), &data_prd); + if (ret != 0) { + kr_log_error("[%s] nghttp2_submit_response failed: %s (%d)\n", server_logstring, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + if((ret = nghttp2_session_send(http_ctx->session)) < 0) { + kr_log_error("[%s] nghttp2_session_send failed: %s (%d)\n", server_logstring, nghttp2_strerror(ret), ret); + return kr_error(EIO); + } + + /* The data is now accepted in gnutls internal buffers, the message can be treated as sent */ + req->handle = (uv_stream_t *)handle; + cb(req, 0); + + return kr_ok(); +} + +void http_free(struct http_ctx_t *ctx) +{ + if (ctx == NULL || ctx->session == NULL) { + return; + } + nghttp2_session_del(ctx->session); + ctx->session = NULL; +} \ No newline at end of file diff --git a/daemon/http.h b/daemon/http.h new file mode 100644 index 000000000..3ab1a233c --- /dev/null +++ b/daemon/http.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2020 CZ.NIC, z.s.p.o + * + * Initial Author: Jan Hák + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include +#include + +/** Transport session (opaque). */ +struct session; + +typedef ssize_t(*http_send_callback)(const uint8_t *buffer, const size_t buffer_len, void *user_ctx); + +struct http_ctx_t { + struct nghttp2_session *session; + http_send_callback send_cb; + void *user_ctx; + int32_t request_stream_id; + uint8_t *wire; + int32_t wire_len; +}; + +struct http_ctx_t* http_new(http_send_callback cb, void *user_ctx); +ssize_t http_process_input_data(struct session *s, const uint8_t *buf, ssize_t nread); +int http_write(uv_write_t *req, uv_handle_t *handle, knot_pkt_t *pkt, uv_write_cb cb); +void http_free(struct http_ctx_t *ctx); \ No newline at end of file diff --git a/daemon/io.c b/daemon/io.c index 7b5f910cb..7154c2f43 100644 --- a/daemon/io.c +++ b/daemon/io.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -12,6 +12,7 @@ #include "daemon/network.h" #include "daemon/worker.h" #include "daemon/tls.h" +#include "daemon/http.h" #include "daemon/session.h" #include "contrib/cleanup.h" #include "lib/utils.h" @@ -183,7 +184,7 @@ int io_listen_udp(uv_loop_t *loop, uv_udp_t *handle, int fd) 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); + struct session *s = session_new(h, false, false); assert(s); session_flags(s)->outgoing = false; @@ -305,6 +306,24 @@ static void tcp_recv(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf) data = session_wirebuf_get_free_start(s); data_len = consumed; } + if (session_flags(s)->has_http) { + consumed = http_process_input_data(s, data, data_len); + if (consumed < 0) { + if (kr_verbose_status) { + struct sockaddr *peer = session_get_peer(s); + char *peer_str = kr_straddr(peer); + kr_log_verbose("[io] => connection to '%s': " + "error processing HTTP data, close\n", + peer_str ? peer_str : ""); + } + worker_end_tcp(s); + return; + } else if (consumed == 0) { + return; + } + data = session_wirebuf_get_free_start(s); + data_len = consumed; + } /* data points to start of the free space in session wire buffer. Simple increase internal counter. */ @@ -320,7 +339,52 @@ static void tcp_recv(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf) mp_flush(the_worker->pkt_pool.ctx); } -static void _tcp_accept(uv_stream_t *master, int status, bool tls) +static void on_write(uv_write_t *req, int status) +{ + struct qr_task *task = req->data; + if (task) { + uv_handle_t *h = (uv_handle_t *)req->handle; + qr_task_on_send(task, h, status); + } + free(req); +} + +static ssize_t tcp_send(const uint8_t *buffer, const size_t buffer_len, void *user_ctx) +{ + //TODO not complete, probably do not respect the sending policy in the software + struct session *session = user_ctx; + uv_handle_t *handle = session_get_handle(session); + //const uint8_t *buffer_backup = (const uint8_t *)calloc(buffer_len, sizeof(*buffer)); + //if (!buffer_backup) { + // return kr_error(EIO); + //} + //memcpy(buffer_backup, buffer, buffer_len); + + uv_write_t *req = (uv_write_t *)calloc(1, sizeof(uv_write_t)); + if (!req) { + return kr_error(EIO); + } + + const uv_buf_t uv_buffer = { + //.base = buffer_backup, + .base = buffer, + .len = buffer_len + }; + uv_write(req, (uv_stream_t *)handle, &uv_buffer, 1, on_write); + return buffer_len; +} + +static ssize_t tls_send(const uint8_t *buffer, const size_t buffer_len, void *user_ctx) +{ + struct tls_ctx_t *ctx = user_ctx; + ssize_t len = 0; + if ((len = gnutls_record_send(ctx->c.tls_session, buffer, buffer_len)) < 0) { + return kr_error(EIO); + } + return len; +} + +static void _tcp_accept(uv_stream_t *master, int status, bool tls, bool http) { if (status != 0) { return; @@ -332,7 +396,7 @@ static void _tcp_accept(uv_stream_t *master, int status, bool tls) return; } int res = io_create(master->loop, (uv_handle_t *)client, - SOCK_STREAM, AF_UNSPEC, tls); + SOCK_STREAM, AF_UNSPEC, tls, http); if (res) { if (res == UV_EMFILE) { worker->too_many_open = true; @@ -396,23 +460,70 @@ static void _tcp_accept(uv_stream_t *master, int status, bool tls) session_tls_set_server_ctx(s, ctx); } } + if (http) { + struct http_ctx_t *ctx = session_http_get_server_ctx(s); + if (!ctx) { + ctx = http_new((tls) ? tls_send : tcp_send, + (tls) ? (void*)session_tls_get_server_ctx(s) : (void*)s + ); + if (!ctx) { + session_close(s); + return; + } + + struct tls_ctx_t *tls_ctx = session_tls_get_server_ctx(s); + if (tls_ctx) { + const gnutls_datum_t protos[] = { + {(unsigned char *)"h2", 2} + }; + ret = gnutls_alpn_set_protocols(tls_ctx->c.tls_session, + protos, sizeof(protos)/sizeof(*protos), + 0); + if (ret != GNUTLS_E_SUCCESS) { + session_close(s); + return; + } + } + session_http_set_server_ctx(s, ctx); + } + } session_timer_start(s, tcp_timeout_trigger, timeout, idle_in_timeout); io_start_read((uv_handle_t *)client); } static void tcp_accept(uv_stream_t *master, int status) { - _tcp_accept(master, status, false); + _tcp_accept(master, status, false, false); } static void tls_accept(uv_stream_t *master, int status) { - _tcp_accept(master, status, true); + _tcp_accept(master, status, true, false); +} + +static void http_accept(uv_stream_t *master, int status) +{ + _tcp_accept(master, status, false, true); } -int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bool has_tls) +static void https_accept(uv_stream_t *master, int status) { - const uv_connection_cb connection = has_tls ? tls_accept : tcp_accept; + _tcp_accept(master, status, true, true); +} + +int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bool has_tls, bool has_http) +{ + uv_connection_cb connection; + if (has_tls && has_http) { + connection = https_accept; + } else if (has_tls) { + connection = tls_accept; + } else if (has_http) { + connection = http_accept; + } else { + connection = tcp_accept; + } + if (!handle) { return kr_error(EINVAL); } @@ -686,7 +797,7 @@ int io_listen_pipe(uv_loop_t *loop, uv_pipe_t *handle, int fd) return 0; } -int io_create(uv_loop_t *loop, uv_handle_t *handle, int type, unsigned family, bool has_tls) +int io_create(uv_loop_t *loop, uv_handle_t *handle, int type, unsigned family, bool has_tls, bool has_http) { int ret = -1; if (type == SOCK_DGRAM) { @@ -698,7 +809,7 @@ int io_create(uv_loop_t *loop, uv_handle_t *handle, int type, unsigned family, b if (ret != 0) { return ret; } - struct session *s = session_new(handle, has_tls); + struct session *s = session_new(handle, has_tls, has_http); if (s == NULL) { ret = -1; } diff --git a/daemon/io.h b/daemon/io.h index eefde8097..541cd2ae6 100644 --- a/daemon/io.h +++ b/daemon/io.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -21,7 +21,7 @@ int io_bind(const struct sockaddr *addr, int type, const endpoint_flags_t *flags /** 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); +int io_listen_tcp(uv_loop_t *loop, uv_tcp_t *handle, int fd, int tcp_backlog, bool has_tls, bool has_http); /** Initialize a pipe handle and start listening. */ int io_listen_pipe(uv_loop_t *loop, uv_pipe_t *handle, int fd); @@ -38,7 +38,7 @@ void tcp_timeout_trigger(uv_timer_t *timer); * \param family = AF_* * \param has_tls has meanings only when type is SOCK_STREAM */ int io_create(uv_loop_t *loop, uv_handle_t *handle, int type, - unsigned family, bool has_tls); + unsigned family, bool has_tls, bool has_http); void io_deinit(uv_handle_t *handle); void io_free(uv_handle_t *handle); diff --git a/daemon/meson.build b/daemon/meson.build index e6f98ebd9..52fad30f8 100644 --- a/daemon/meson.build +++ b/daemon/meson.build @@ -10,6 +10,7 @@ kresd_src = files([ 'bindings/worker.c', 'engine.c', 'ffimodule.c', + 'http.c', 'io.c', 'main.c', 'network.c', @@ -17,6 +18,7 @@ kresd_src = files([ 'tls.c', 'tls_ephemeral_credentials.c', 'tls_session_ticket-srv.c', + 'http.c', 'udp_queue.c', 'worker.c', 'zimport.c', @@ -43,6 +45,7 @@ kresd_deps = [ gnutls, libsystemd, capng, + nghttp2, ] diff --git a/daemon/network.c b/daemon/network.c index ee8dc56e4..bf63b1751 100644 --- a/daemon/network.c +++ b/daemon/network.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2015-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -312,7 +312,7 @@ static int open_endpoint(struct network *net, struct endpoint *ep, return kr_error(ENOMEM); } return io_listen_tcp(net->loop, ep_handle, ep->fd, - net->tcp_backlog, ep->flags.tls); + net->tcp_backlog, ep->flags.tls, ep->flags.http); } /* else */ assert(!EINVAL); diff --git a/daemon/network.h b/daemon/network.h index c3ee79f2a..a960effa4 100644 --- a/daemon/network.h +++ b/daemon/network.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2015-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -20,6 +20,7 @@ struct engine; typedef struct { int sock_type; /**< SOCK_DGRAM or SOCK_STREAM */ bool tls; /**< only used together with .kind == NULL and .tcp */ + bool http; /**< only used together with .kind == NULL and .tcp */ const char *kind; /**< tag for other types than the three usual */ bool freebind; /**< used for binding to non-local address **/ } endpoint_flags_t; diff --git a/daemon/session.c b/daemon/session.c index 616e85195..48ceafeae 100644 --- a/daemon/session.c +++ b/daemon/session.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 CZ.NIC, z.s.p.o. +/* Copyright (C) 2018-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -10,6 +10,7 @@ #include "daemon/session.h" #include "daemon/engine.h" #include "daemon/tls.h" +#include "daemon/http.h" #include "daemon/worker.h" #include "daemon/io.h" #include "lib/generic/queue.h" @@ -32,6 +33,8 @@ struct session { struct tls_ctx *tls_ctx; /**< server side tls-related data. */ struct tls_client_ctx *tls_client_ctx; /**< client side tls-related data. */ + struct http_ctx_t *http_ctx; /**< server side http-related data. */ + trie_t *tasks; /**< list of tasks assotiated with given session. */ queue_t(struct qr_task *) waiting; /**< list of tasks waiting for sending to upstream. */ @@ -81,6 +84,7 @@ void session_clear(struct session *session) queue_deinit(session->waiting); tls_free(session->tls_ctx); tls_client_ctx_free(session->tls_client_ctx); + http_free(session->http_ctx); memset(session, 0, sizeof(*session)); } @@ -284,6 +288,16 @@ struct tls_common_ctx *session_tls_get_common_ctx(const struct session *session) return tls_ctx; } +struct http_ctx_t *session_http_get_server_ctx(const struct session *session) +{ + return session->http_ctx; +} + +void session_http_set_server_ctx(struct session *session, struct http_ctx_t *ctx) +{ + session->http_ctx = ctx; +} + uv_handle_t *session_get_handle(struct session *session) { return session->handle; @@ -294,7 +308,7 @@ struct session *session_get(uv_handle_t *h) return h->data; } -struct session *session_new(uv_handle_t *handle, bool has_tls) +struct session *session_new(uv_handle_t *handle, bool has_tls, bool has_http) { if (!handle) { return NULL; @@ -314,6 +328,9 @@ struct session *session_new(uv_handle_t *handle, bool has_tls) wire_buffer_size += TLS_CHUNK_SIZE; session->sflags.has_tls = true; } + if (has_http) { + session->sflags.has_http = true; + } uint8_t *wire_buf = malloc(wire_buffer_size); if (!wire_buf) { free(session); diff --git a/daemon/session.h b/daemon/session.h index a36eb6134..e71653fb3 100644 --- a/daemon/session.h +++ b/daemon/session.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 CZ.NIC, z.s.p.o. +/* Copyright (C) 2018-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -17,6 +17,7 @@ struct session_flags { bool outgoing : 1; /**< True: to upstream; false: from a client. */ bool throttled : 1; /**< True: data reading from peer is temporarily stopped. */ bool has_tls : 1; /**< True: given session uses TLS. */ + bool has_http : 1; /**< True: given session uses HTTP. */ bool connected : 1; /**< True: TCP connection is established. */ bool closing : 1; /**< True: session close sequence is in progress. */ bool wirebuf_error : 1; /**< True: last operation with wirebuf ended up with an error. */ @@ -24,7 +25,7 @@ struct session_flags { /* Allocate new session for a libuv handle. * If handle->tyoe is UV_UDP, tls parameter will be ignored. */ -struct session *session_new(uv_handle_t *handle, bool has_tls); +struct session *session_new(uv_handle_t *handle, bool has_tls, bool has_http); /* Clear and free given session. */ void session_free(struct session *session); /* Clear session. */ @@ -95,6 +96,11 @@ void session_tls_set_client_ctx(struct session *session, struct tls_client_ctx * * server and client. */ struct tls_common_ctx *session_tls_get_common_ctx(const struct session *session); +/** Get pointer to server-side http-related data. */ +struct http_ctx_t *session_http_get_server_ctx(const struct session *session); +/** Set pointer to server-side http-related data. */ +void session_http_set_server_ctx(struct session *session, struct http_ctx_t *ctx); + /** Get pointer to underlying libuv handle for IO operations. */ uv_handle_t *session_get_handle(struct session *session); struct session *session_get(uv_handle_t *h); diff --git a/daemon/worker.c b/daemon/worker.c index dc262003a..47bb3b0e1 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -26,6 +26,7 @@ #include "daemon/io.h" #include "daemon/session.h" #include "daemon/tls.h" +#include "daemon/http.h" #include "daemon/udp_queue.h" #include "daemon/zimport.h" #include "lib/layer.h" @@ -128,7 +129,8 @@ struct worker_ctx *the_worker = NULL; /*! @internal Create a UDP/TCP handle for an outgoing AF_INET* connection. * socktype is SOCK_* */ static uv_handle_t *ioreq_spawn(struct worker_ctx *worker, - int socktype, sa_family_t family, bool has_tls) + int socktype, sa_family_t family, bool has_tls, + bool has_http) { bool precond = (socktype == SOCK_DGRAM || socktype == SOCK_STREAM) && (family == AF_INET || family == AF_INET6); @@ -144,7 +146,7 @@ static uv_handle_t *ioreq_spawn(struct worker_ctx *worker, if (!handle) { return NULL; } - int ret = io_create(worker->loop, handle, socktype, family, has_tls); + int ret = io_create(worker->loop, handle, socktype, family, has_tls, has_http); if (ret) { if (ret == UV_EMFILE) { worker->too_many_open = true; @@ -295,6 +297,7 @@ static struct request_ctx *request_create(struct worker_ctx *worker, req->qsource.dst_addr = session_get_sockname(session); req->qsource.flags.tcp = session_get_handle(session)->type == UV_TCP; req->qsource.flags.tls = session_flags(session)->has_tls; + req->qsource.flags.http = session_flags(session)->has_http; /* We need to store a copy of peer address. */ memcpy(&ctx->source.addr.ip, peer, kr_sockaddr_len(peer)); req->qsource.addr = &ctx->source.addr.ip; @@ -606,7 +609,11 @@ static int qr_task_send(struct qr_task *task, struct session *session, struct worker_ctx *worker = ctx->worker; /* Send using given protocol */ assert(!session_flags(session)->closing); - if (session_flags(session)->has_tls) { + if (session_flags(session)->has_http) { + uv_write_t *write_req = (uv_write_t *)ioreq; + write_req->data = task; + ret = http_write(write_req, handle, pkt, &on_write); + } else if (session_flags(session)->has_tls) { uv_write_t *write_req = (uv_write_t *)ioreq; write_req->data = task; ret = tls_write(write_req, handle, pkt, &on_write); @@ -1034,7 +1041,7 @@ static uv_handle_t *retransmit(struct qr_task *task) if (kr_resolve_checkout(&ctx->req, NULL, (struct sockaddr *)choice, SOCK_DGRAM, task->pktbuf) != 0) { return ret; } - ret = ioreq_spawn(ctx->worker, SOCK_DGRAM, choice->sin6_family, false); + ret = ioreq_spawn(ctx->worker, SOCK_DGRAM, choice->sin6_family, false, false); if (!ret) { return ret; } @@ -1317,8 +1324,9 @@ static int tcp_task_make_connection(struct qr_task *task, const struct sockaddr tls_client_ctx_free(tls_ctx); return kr_error(EINVAL); } + bool has_http = false; bool has_tls = (tls_ctx != NULL); - uv_handle_t *client = ioreq_spawn(worker, SOCK_STREAM, addr->sa_family, has_tls); + uv_handle_t *client = ioreq_spawn(worker, SOCK_STREAM, addr->sa_family, has_tls, has_http); if (!client) { tls_client_ctx_free(tls_ctx); free(conn); diff --git a/lib/defines.h b/lib/defines.h index be2d3a949..2b13b7695 100644 --- a/lib/defines.h +++ b/lib/defines.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. +/* Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -59,6 +59,8 @@ static inline int KR_COLD kr_error(int x) { * Defines. */ #define KR_DNS_PORT 53 +#define KR_DNS_HTTP_PORT 80 +#define KR_DNS_DOH_PORT 443 #define KR_DNS_TLS_PORT 853 #define KR_EDNS_VERSION 0 #define KR_EDNS_PAYLOAD 4096 /* Default UDP payload (max unfragmented UDP is 1452B) */ diff --git a/meson.build b/meson.build index bf43d458b..58abd209d 100644 --- a/meson.build +++ b/meson.build @@ -29,6 +29,7 @@ if not lmdb.found() # darwin workaround: missing pkgconfig endif gnutls = dependency('gnutls') luajit = dependency('luajit') +nghttp2 = dependency('libnghttp2') # NOTE avoid using link_args for luajit due to a macOS issue # https://github.com/Homebrew/homebrew-core/issues/37169 luajit_inc = luajit.partial_dependency(compile_args: true, includes: true)