/* * Copyright (c) 2020, CZ.NIC, z.s.p.o. * All rights reserved. * * This file is part of dnsjit. * * dnsjit 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. * * dnsjit 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 dnsjit. If not, see . */ #include "config.h" #include "output/dnssim.h" #include "output/dnssim/internal.h" #include "output/dnssim/ll.h" #include "core/assert.h" #include "lib/base64url.h" #include #include #if GNUTLS_VERSION_NUMBER >= DNSSIM_MIN_GNUTLS_VERSION #define OUTPUT_DNSSIM_MAKE_NV(NAME, VALUE, VALUELEN) \ { \ (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN, \ NGHTTP2_NV_FLAG_NONE \ } #define OUTPUT_DNSSIM_MAKE_NV2(NAME, VALUE) \ { \ (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ NGHTTP2_NV_FLAG_NONE \ } #define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE "?dns=" #define OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN (sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1) #define OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS 100 #define OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS 0xffffffffu static core_log_t _log = LOG_T_INIT("output.dnssim"); static ssize_t _http2_send(nghttp2_session* session, const uint8_t* data, size_t length, int flags, void* user_data) { _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; mlassert(conn, "conn can't be null"); mlassert(conn->tls, "conn must have tls ctx"); mlassert(conn->tls->session, "conn must have tls session"); mldebug("http2 (%p): sending data, len=%ld", session, length); ssize_t len = 0; if ((len = gnutls_record_send(conn->tls->session, data, length)) < 0) { mlwarning("gnutls_record_send failed: %s", gnutls_strerror(len)); len = NGHTTP2_ERR_CALLBACK_FAILURE; } return len; } static ssize_t _http2_on_data_provider_read(nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) { _output_dnssim_https2_data_provider_t* buffer = source->ptr; mlassert(buffer, "no data provider"); mlassert(buffer->len <= MAX_DNSMSG_SIZE, "invalid dnsmsg size: %zu B", buffer->len); ssize_t sent = (length < buffer->len) ? length : buffer->len; mlassert(sent >= 0, "negative length of bytes to send"); memcpy(buf, buffer->buf, sent); buffer->buf += sent; buffer->len -= sent; if (buffer->len == 0) *data_flags |= NGHTTP2_DATA_FLAG_EOF; return sent; } static _output_dnssim_query_tcp_t* _http2_get_stream_qry(_output_dnssim_connection_t* conn, int32_t stream_id) { mlassert(conn, "conn is nil"); mlassert(stream_id >= 0, "invalid stream_id"); _output_dnssim_query_tcp_t* qry = (_output_dnssim_query_tcp_t*)conn->sent; while (qry != NULL && qry->stream_id != stream_id) { qry = (_output_dnssim_query_tcp_t*)qry->qry.next; } return qry; } static int _http2_on_header(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) { if (frame->hd.type == NGHTTP2_HEADERS && frame->headers.cat == NGHTTP2_HCAT_RESPONSE) { if (namelen == 7 && strncmp((char*)name, ":status", 7) == 0) { if (valuelen != 3 || (value[0] != '1' && value[0] != '2')) { /* When reponse code isn't 1xx or 2xx, close the query. * This will result in request timeout, which currently seems * slightly better than mocking SERVFAIL for statistics. */ _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; mlassert(conn, "conn is nil"); _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id); if (qry != NULL) { _output_dnssim_close_query_https2(qry); mlinfo("http response %s, closing query", value); } } } } return 0; } static int _http2_on_data_recv(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) { _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; mlassert(conn, "conn is nil"); _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, stream_id); mldebug("http2: data chunk recv, session=%p, len=%d", session, len); if (qry) { if (qry->recv_buf_len == 0) { if (len > MAX_DNSMSG_SIZE) { mlwarning("http response exceeded maximum size of dns message"); return -1; } mlfatal_oom(qry->recv_buf = malloc(len)); memcpy(qry->recv_buf, data, len); qry->recv_buf_len = len; } else { size_t total_len = qry->recv_buf_len + len; if (total_len > MAX_DNSMSG_SIZE) { mlwarning("http response exceeded maximum size of dns message"); return -1; } mlfatal_oom(qry->recv_buf = realloc(qry->recv_buf, total_len)); memcpy(qry->recv_buf + qry->recv_buf_len, data, len); qry->recv_buf_len = total_len; } } else { mldebug("no query associated with this stream id, ignoring"); } return 0; } static void _http2_check_max_streams(_output_dnssim_connection_t* conn) { mlassert(conn, "conn can't be null"); mlassert(conn->http2, "conn must have http2 ctx"); switch (conn->state) { case _OUTPUT_DNSSIM_CONN_ACTIVE: if (conn->http2->open_streams >= conn->http2->max_concurrent_streams) { mlinfo("http2 (%p): reached maximum number of concurrent streams (%ld)", conn->http2->session, conn->http2->max_concurrent_streams); conn->state = _OUTPUT_DNSSIM_CONN_CONGESTED; } break; case _OUTPUT_DNSSIM_CONN_CONGESTED: if (conn->http2->open_streams < conn->http2->max_concurrent_streams) conn->state = _OUTPUT_DNSSIM_CONN_ACTIVE; break; default: break; } } static int _http2_on_stream_close(nghttp2_session* session, int32_t stream_id, uint32_t error_code, void* user_data) { _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; mlassert(conn, "conn can't be null"); mlassert(conn->http2, "conn must have http2 ctx"); mlassert(conn->http2->open_streams > 0, "conn has no open streams"); conn->http2->open_streams--; _http2_check_max_streams(conn); return 0; } static int _http2_on_frame_recv(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { _output_dnssim_connection_t* conn = (_output_dnssim_connection_t*)user_data; mlassert(conn, "conn can't be null"); mlassert(conn->tls, "conn must have tls ctx"); mlassert(conn->tls->session, "conn must have tls session"); mlassert(conn->http2, "conn must have http2 ctx"); switch (frame->hd.type) { case NGHTTP2_DATA: if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { mldebug("http2 (%p): final DATA frame recv", session); _output_dnssim_query_tcp_t* qry = _http2_get_stream_qry(conn, frame->hd.stream_id); if (qry != NULL) { conn->http2->current_qry = qry; _output_dnssim_read_dnsmsg(conn, qry->recv_buf_len, (char*)qry->recv_buf); } } break; case NGHTTP2_SETTINGS: if (!conn->http2->remote_settings_received) { /* On the first SETTINGS frame, set concurrent streams to unlimited, same as nghttp2. */ conn->http2->remote_settings_received = true; conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_DEFAULT_MAX_CONCURRENT_STREAMS; _http2_check_max_streams(conn); } nghttp2_settings* settings = (nghttp2_settings*)frame; int i; for (i = 0; i < settings->niv; i++) { switch (settings->iv[i].settings_id) { case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: conn->http2->max_concurrent_streams = settings->iv[i].value; _http2_check_max_streams(conn); break; default: break; } } break; default: break; } return 0; } int _output_dnssim_https2_init(_output_dnssim_connection_t* conn) { mlassert(conn, "conn is nil"); mlassert(conn->tls == NULL, "conn already has tls context"); mlassert(conn->http2 == NULL, "conn already has http2 context"); mlassert(conn->client, "conn must be associated with a client"); mlassert(conn->client->dnssim, "client must have dnssim"); int ret = -1; nghttp2_session_callbacks* callbacks; nghttp2_option* option; output_dnssim_t* self = conn->client->dnssim; /* Initialize TLS session. */ ret = _output_dnssim_tls_init(conn); if (ret < 0) return ret; /* Configure ALPN to negotiate HTTP/2. */ const gnutls_datum_t protos[] = { { (unsigned char*)"h2", 2 } }; ret = gnutls_alpn_set_protocols(conn->tls->session, protos, 1, 0); if (ret < 0) { lwarning("failed to set ALPN protocol: %s", gnutls_strerror(ret)); return ret; } lfatal_oom(conn->http2 = calloc(1, sizeof(_output_dnssim_http2_ctx_t))); conn->http2->max_concurrent_streams = OUTPUT_DNSSIM_HTTP2_INITIAL_MAX_CONCURRENT_STREAMS; /* Set up HTTP/2 callbacks and client. */ lassert(nghttp2_session_callbacks_new(&callbacks) == 0, "out of memory"); nghttp2_session_callbacks_set_send_callback(callbacks, _http2_send); nghttp2_session_callbacks_set_on_header_callback(callbacks, _http2_on_header); nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, _http2_on_data_recv); nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, _http2_on_frame_recv); nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, _http2_on_stream_close); lassert(nghttp2_option_new(&option) == 0, "out of memory"); nghttp2_option_set_peer_max_concurrent_streams(option, conn->http2->max_concurrent_streams); ret = nghttp2_session_client_new2(&conn->http2->session, callbacks, conn, option); nghttp2_session_callbacks_del(callbacks); nghttp2_option_del(option); if (ret < 0) { free(conn->http2); conn->http2 = NULL; } return ret; } int _output_dnssim_https2_setup(_output_dnssim_connection_t* conn) { mlassert(conn, "conn is nil"); mlassert(conn->tls, "conn must have tls ctx"); mlassert(conn->tls->session, "conn must have tls session"); mlassert(conn->http2, "conn must have http2 ctx"); mlassert(conn->http2->session, "conn must have http2 session"); int ret = -1; /* Check "h2" protocol was negotiated with ALPN. */ gnutls_datum_t proto; ret = gnutls_alpn_get_selected_protocol(conn->tls->session, &proto); if (ret < 0) { mlwarning("http2: failed to get negotiated protocol: %s", gnutls_strerror(ret)); return ret; } if (proto.size != 2 || memcmp("h2", proto.data, 2) != 0) { mlwarning("http2: protocol is not negotiated"); return ret; } /* Submit SETTIGNS frame. */ static const nghttp2_settings_entry iv[] = { { NGHTTP2_SETTINGS_MAX_FRAME_SIZE, MAX_DNSMSG_SIZE }, { NGHTTP2_SETTINGS_ENABLE_PUSH, 0 }, /* Only we can initiate streams. */ }; ret = nghttp2_submit_settings(conn->http2->session, NGHTTP2_FLAG_NONE, iv, sizeof(iv) / sizeof(*iv)); if (ret < 0) { mlwarning("http2: failed to submit SETTINGS: %s", nghttp2_strerror(ret)); return ret; } ret = 0; return ret; } void _output_dnssim_https2_process_input_data(_output_dnssim_connection_t* conn, size_t len, const char* data) { mlassert(conn, "conn is nil"); mlassert(conn->http2, "conn must have http2 ctx"); mlassert(conn->http2->session, "conn must have http2 session"); /* Process incoming frames. */ ssize_t ret = 0; conn->prevent_close = true; ret = nghttp2_session_mem_recv(conn->http2->session, (uint8_t*)data, len); conn->prevent_close = false; if (ret < 0) { mlwarning("failed nghttp2_session_mem_recv: %s", nghttp2_strerror(ret)); _output_dnssim_conn_close(conn); return; } else if (conn->state == _OUTPUT_DNSSIM_CONN_CLOSE_REQUESTED) { _output_dnssim_conn_close(conn); return; } mlassert(ret == len, "nghttp2_session_mem_recv didn't process all data"); /* Send any frames the read might have triggered. */ ret = nghttp2_session_send(conn->http2->session); if (ret < 0) { mlwarning("failed nghttp2_session_send: %s", nghttp2_strerror(ret)); _output_dnssim_conn_close(conn); return; } } int _output_dnssim_create_query_https2(output_dnssim_t* self, _output_dnssim_request_t* req) { mlassert_self(); lassert(req, "req is nil"); lassert(req->client, "request must have a client associated with it"); _output_dnssim_query_tcp_t* qry; lfatal_oom(qry = calloc(1, sizeof(_output_dnssim_query_tcp_t))); qry->qry.transport = OUTPUT_DNSSIM_TRANSPORT_HTTPS2; qry->qry.req = req; qry->qry.state = _OUTPUT_DNSSIM_QUERY_PENDING_WRITE; qry->stream_id = -1; req->qry = &qry->qry; // TODO change when adding support for multiple Qs for req _ll_append(req->client->pending, &qry->qry); return _output_dnssim_handle_pending_queries(req->client); } void _output_dnssim_close_query_https2(_output_dnssim_query_tcp_t* qry) { mlassert(qry, "qry can't be null"); mlassert(qry->qry.req, "query must be part of a request"); _output_dnssim_request_t* req = qry->qry.req; mlassert(req->client, "request must belong to a client"); _ll_try_remove(req->client->pending, &qry->qry); if (qry->conn) { _output_dnssim_connection_t* conn = qry->conn; _ll_try_remove(conn->sent, &qry->qry); qry->conn = NULL; _output_dnssim_conn_idle(conn); } if (qry->recv_buf != NULL) free(qry->recv_buf); _ll_remove(req->qry, &qry->qry); free(qry); } void _output_dnssim_https2_close(_output_dnssim_connection_t* conn) { mlassert(conn, "conn can't be nil"); mlassert(conn->http2, "conn must have http2 ctx"); nghttp2_session_del(conn->http2->session); _output_dnssim_tls_close(conn); } static int _http2_send_query_get(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) { mlassert(conn, "conn can't be null"); mlassert(qry, "qry can't be null"); mlassert(qry->qry.req, "req can't be null"); mlassert(qry->qry.req->payload, "payload can't be null"); mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big"); mlassert(conn->client, "conn must be associated with client"); mlassert(conn->client->dnssim, "client must have dnssim"); output_dnssim_t* self = conn->client->dnssim; core_object_payload_t* content = qry->qry.req->payload; const size_t uri_path_len = strlen(_self->h2_uri_path); const size_t path_len = uri_path_len + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN + (content->len * 4) / 3 + 3 /* upper limit of base64 encoding */ + 1; /* terminating null byte */ if (path_len >= _MAX_URI_LEN) { self->discarded++; linfo("http2: uri path with query too long, query discarded"); return 0; } char path[path_len]; memcpy(path, _self->h2_uri_path, uri_path_len); memcpy(&path[uri_path_len], OUTPUT_DNSSIM_HTTP_GET_TEMPLATE, OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN); int32_t ret = base64url_encode(content->payload, content->len, (uint8_t*)&path[uri_path_len + OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN], sizeof(path) - uri_path_len - OUTPUT_DNSSIM_HTTP_GET_TEMPLATE_LEN - 1); if (ret < 0) { self->discarded++; linfo("http2: base64url encode of query failed, query discarded"); return 0; } nghttp2_nv hdrs[] = { OUTPUT_DNSSIM_MAKE_NV2(":method", "GET"), OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"), OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)), OUTPUT_DNSSIM_MAKE_NV(":path", path, uri_path_len + sizeof(OUTPUT_DNSSIM_HTTP_GET_TEMPLATE) - 1 + ret), OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"), }; qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), NULL, NULL); if (qry->stream_id < 0) { mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id)); return -1; } mldebug("http2 (%p): GET %s", conn->http2->session, path); conn->http2->open_streams++; _http2_check_max_streams(conn); ret = nghttp2_session_send(conn->http2->session); if (ret < 0) { mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret)); return -1; } return 0; } static int _http2_send_query_post(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) { mlassert(conn, "conn can't be null"); mlassert(qry, "qry can't be null"); mlassert(qry->qry.req, "req can't be null"); mlassert(qry->qry.req->payload, "payload can't be null"); mlassert(qry->qry.req->payload->len <= MAX_DNSMSG_SIZE, "payload too big"); mlassert(conn->client, "conn must be associated with client"); mlassert(conn->client->dnssim, "client must have dnssim"); output_dnssim_t* self = conn->client->dnssim; core_object_payload_t* content = qry->qry.req->payload; int window_size = nghttp2_session_get_remote_window_size(conn->http2->session); if (content->len > window_size) { mldebug("http2 (%p): insufficient remote window size, deferring", conn->http2->session); return 0; } char content_length[6]; /* max dnslen "65535" */ int content_length_len = snprintf(content_length, 6, "%zd", content->len); nghttp2_nv hdrs[] = { OUTPUT_DNSSIM_MAKE_NV2(":method", "POST"), OUTPUT_DNSSIM_MAKE_NV2(":scheme", "https"), OUTPUT_DNSSIM_MAKE_NV(":authority", _self->h2_uri_authority, strlen(_self->h2_uri_authority)), OUTPUT_DNSSIM_MAKE_NV(":path", _self->h2_uri_path, strlen(_self->h2_uri_path)), OUTPUT_DNSSIM_MAKE_NV2("accept", "application/dns-message"), OUTPUT_DNSSIM_MAKE_NV2("content-type", "application/dns-message"), OUTPUT_DNSSIM_MAKE_NV("content-length", content_length, content_length_len) }; _output_dnssim_https2_data_provider_t data = { .buf = content->payload, .len = content->len }; nghttp2_data_provider data_provider = { .source.ptr = &data, .read_callback = _http2_on_data_provider_read }; qry->stream_id = nghttp2_submit_request(conn->http2->session, NULL, hdrs, sizeof(hdrs) / sizeof(nghttp2_nv), &data_provider, NULL); if (qry->stream_id < 0) { mldebug("http2 (%p): failed to submit request: %s", conn->http2->session, nghttp2_strerror(qry->stream_id)); return -1; } mldebug("http2 (%p): POST payload len=%ld", conn->http2->session, content->len); conn->http2->open_streams++; _http2_check_max_streams(conn); window_size = nghttp2_session_get_stream_remote_window_size(conn->http2->session, qry->stream_id); mlassert(content->len <= window_size, "unsupported: http2 stream window size (%ld B) is smaller than dns payload (%ld B)", window_size, content->len); int ret = nghttp2_session_send(conn->http2->session); if (ret < 0) { mldebug("http2 (%p): failed session send: %s", conn->http2->session, nghttp2_strerror(ret)); return -1; } return 0; } void _output_dnssim_https2_write_query(_output_dnssim_connection_t* conn, _output_dnssim_query_tcp_t* qry) { mlassert(qry, "qry can't be null"); mlassert(qry->qry.state == _OUTPUT_DNSSIM_QUERY_PENDING_WRITE, "qry must be pending write"); mlassert(conn, "conn can't be null"); mlassert(conn->state == _OUTPUT_DNSSIM_CONN_ACTIVE, "connection state != ACTIVE"); mlassert(conn->http2, "conn must have http2 ctx"); mlassert(conn->http2->session, "conn must have http2 session"); mlassert(conn->client, "conn must be associated with client"); mlassert(conn->client->pending, "conn has no pending queries"); mlassert(conn->client->dnssim, "client must have dnssim"); int ret = 0; output_dnssim_t* self = conn->client->dnssim; if (!nghttp2_session_check_request_allowed(conn->http2->session)) { mldebug("http2 (%p): request not allowed", conn->http2->session); _output_dnssim_conn_close(conn); return; } switch (_self->h2_method) { case OUTPUT_DNSSIM_H2_POST: ret = _http2_send_query_post(conn, qry); break; case OUTPUT_DNSSIM_H2_GET: ret = _http2_send_query_get(conn, qry); break; default: lfatal("http2: unsupported method"); } if (ret < 0) { _output_dnssim_conn_close(conn); return; } qry->conn = conn; _ll_remove(conn->client->pending, &qry->qry); _ll_append(conn->sent, &qry->qry); /* Stop idle timer, since there are queries to answer now. */ if (conn->idle_timer != NULL) { conn->is_idle = false; uv_timer_stop(conn->idle_timer); } qry->qry.state = _OUTPUT_DNSSIM_QUERY_SENT; } #endif