From: Daniel Kahn Gillmor Date: Sun, 3 Apr 2016 13:13:24 +0000 (-0300) Subject: WIP: first pass at TLS implementation X-Git-Tag: v1.1.0~7^2~22 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4330e95ae1d961f794d68a45512056e5c19b9d0d;p=thirdparty%2Fknot-resolver.git WIP: first pass at TLS implementation --- diff --git a/daemon/daemon.mk b/daemon/daemon.mk index 6a3f1f90e..693e501c7 100644 --- a/daemon/daemon.mk +++ b/daemon/daemon.mk @@ -5,6 +5,7 @@ kresd_SOURCES := \ daemon/worker.c \ daemon/bindings.c \ daemon/ffimodule.c \ + daemon/tls.c \ daemon/main.c kresd_DIST := daemon/lua/kres.lua daemon/lua/trust_anchors.lua diff --git a/daemon/io.c b/daemon/io.c index 4058029fc..1a69dbeeb 100644 --- a/daemon/io.c +++ b/daemon/io.c @@ -23,6 +23,7 @@ #include "daemon/io.h" #include "daemon/network.h" #include "daemon/worker.h" +#include "daemon/tls.h" #define negotiate_bufsize(func, handle, bufsize_want) do { \ int bufsize = 0; func(handle, &bufsize); \ @@ -49,6 +50,7 @@ static void session_clear(struct session *s) { assert(s->outgoing || s->tasks.len == 0); array_clear(s->tasks); + tls_free(s->tls_ctx); memset(s, 0, sizeof(*s)); } @@ -204,7 +206,12 @@ static void tcp_recv(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf) struct worker_ctx *worker = loop->data; /* TCP pipelining is rather complicated and requires cooperation from the worker * so the whole message reassembly and demuxing logic is inside worker */ - int ret = worker_process_tcp(worker, (uv_handle_t *)handle, (const uint8_t *)buf->base, nread); + int ret = 0; + if (s->has_tls) { + ret = worker_process_tls(worker, handle, (const uint8_t *)buf->base, nread); + } else { + ret = worker_process_tcp(worker, handle, (const uint8_t *)buf->base, nread); + } if (ret < 0) { worker_end_tcp(worker, (uv_handle_t *)handle); /* Exceeded per-connection quota for outstanding requests @@ -245,6 +252,9 @@ static void _tcp_accept(uv_stream_t *master, int status, bool tls) * is idle and should be terminated, this is an educated guess. */ struct session *session = client->data; session->has_tls = tls; + if (tls && !session->tls_ctx) { + session->tls_ctx = tls_new(master->loop->data); + } uv_timer_t *timer = &session->timeout; uv_timer_init(master->loop, timer); timer->data = client; diff --git a/daemon/io.h b/daemon/io.h index ba29cf1e2..a85df52dd 100644 --- a/daemon/io.h +++ b/daemon/io.h @@ -21,6 +21,7 @@ #include "lib/generic/array.h" struct qr_task; +struct tls_ctx_t; /* Per-session (TCP or UDP) persistent structure, * that exists between remote counterpart and a local socket. @@ -31,6 +32,7 @@ struct session { bool has_tls; uv_timer_t timeout; struct qr_task *buffering; + struct tls_ctx_t *tls_ctx; array_t(struct qr_task *) tasks; }; diff --git a/daemon/tls.c b/daemon/tls.c new file mode 100644 index 000000000..ea6ecdb78 --- /dev/null +++ b/daemon/tls.c @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2016 American Civil Liberties Union (ACLU) + * + * Initial Author: Daniel Kahn Gillmor + * + * 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 +#include +#include +#include + +#include "daemon/worker.h" +#include "daemon/tls.h" +#include "daemon/io.h" + +static const char *priorities = "NORMAL"; + +/* gnutls_record_recv and gnutls_record_send */ +struct tls_ctx_t { + gnutls_session_t session; + gnutls_certificate_credentials_t x509_creds; + int handshake_done; + + uv_stream_t *handle; + + /* for reading from the network */ + const uint8_t *buf; + ssize_t nread; + ssize_t consumed; + uint8_t recv_buf[4096]; + + /* for writing to the network */ + uv_write_t *writer; + uv_write_cb write_cb; +}; + +/** @internal Debugging facility. */ +#ifdef DEBUG +#define DEBUG_MSG(fmt...) fprintf(stderr, "[daem] " fmt) +#else +#define DEBUG_MSG(fmt...) +#endif + +static void +kres_gnutls_log(int level, const char *message) +{ + kr_log_error("[tls] gnutls: (%d) %s", level, message); +} + + +static ssize_t +kres_gnutls_push(gnutls_transport_ptr_t h, const void *buf, size_t len) +{ + struct tls_ctx_t *t = (struct tls_ctx_t *)h; + const uv_buf_t ub = {(void *)buf, len}; + int ret; + + DEBUG_MSG("[tls] push %zu <%p>\n", len, h); + if (t == NULL) { + errno = EFAULT; + return -1; + } + ret = uv_try_write(t->handle, &ub, 1); + + if (ret > 0) + return (ssize_t) ret; + if (ret == UV_EAGAIN) + errno = EAGAIN; + else { + kr_log_error("[tls] uv_try_write unknown error: %d\n", ret); + errno = EIO; /* dkg just picked this at random */ + } + return -1; +} + + +static ssize_t +kres_gnutls_pull(gnutls_transport_ptr_t h, void *buf, size_t len) +{ + struct tls_ctx_t *t = (struct tls_ctx_t *)h; + ssize_t avail = t->nread - t->consumed; + ssize_t transfer; + DEBUG_MSG("[tls] pull wanted: %zu available: %zu\n", len, avail); + + if (t->nread <= t->consumed) { + errno = EAGAIN; + return -1; + } + if (avail <= len) + transfer = avail; + else + transfer = len; + + memcpy(buf, t->buf + t->consumed, transfer); + t->consumed += transfer; + return transfer; +} + +static ssize_t +kres_gnutls_push_vec(gnutls_transport_ptr_t h, const giovec_t * iov, int iovcnt) +{ + struct tls_ctx_t *t = (struct tls_ctx_t *)h; + int ret; + + DEBUG_MSG("vecpush %d (%p) handle: <%p> writer <%p>\n", iovcnt, iov, t->handle, t->writer); + + /* + * because of the struct of giovec_t is identical to struct iovec; + * and uv_buf_t header (uv-unix.h) says it may be cast to struct + * iovec; so we should be able to just cast directly. + */ + ret = uv_write(t->writer, t->handle, (uv_buf_t *) iov, iovcnt, t->write_cb); + if (ret >= 0) { + /* Pending ioreq on current task */ + return (ssize_t) ret; + } + switch (ret) { + case UV_EAGAIN: + errno = EAGAIN; + break; + case UV_EINTR: + errno = EINTR; + break; + default: + kr_log_error("[tls] uv_write unknown error: %d\n", ret); + errno = EIO; /* dkg just picked this at random */ + } + return -1; +} + +struct tls_ctx_t * +tls_new(struct worker_ctx *worker) +{ + int err; + struct tls_ctx_t *t; + struct network *net = &worker->engine->net; + const char *errpos; + + if (!net->tls_cert) { + kr_log_error("[tls] net.tls_cert is missing; no TLS\n"); + } + if (!net->tls_key) { + kr_log_error("[tls] net.tls_key is missing; no TLS\n"); + } + if (!net->tls_key || !net->tls_cert) { + return NULL; + } + t = calloc(1, sizeof(struct tls_ctx_t)); + + if (t == NULL) { + kr_log_error("[tls] failed to allocate TLS context\n"); + return NULL; + } + /* FIXME: this should only be done once on the daemon */ + /* FIXME: propagate verbosity here? */ + gnutls_global_set_log_function(kres_gnutls_log); + gnutls_global_set_log_level(0); + + /* + * FIXME: credentials should be global, instead of per-session; but + * then we would have to keep track of which sessions use them before + * changing them dyamically + */ + if ((err = gnutls_certificate_allocate_credentials(&t->x509_creds))) { + kr_log_error("[tls] gnutls_certificate_allocate_credentials() failed: (%d) %s\n", + err, gnutls_strerror_name(err)); + tls_free(t); + return NULL; + } + err = gnutls_certificate_set_x509_system_trust(t->x509_creds); + if (err < 0) { + kr_log_error("[tls] warning: gnutls_certificate_set_x509_system_trust() failed: (%d) %s\n", + err, gnutls_strerror_name(err)); + } + if ((err = gnutls_certificate_set_x509_key_file(t->x509_creds, net->tls_cert, + net->tls_key, GNUTLS_X509_FMT_PEM))) { + kr_log_error("[tls] gnutls_certificate_set_x509_key_file(%s,%s) failed: %d (%s)\n", + net->tls_cert, net->tls_key, err, gnutls_strerror_name(err)); + tls_free(t); + return NULL; + } + if ((err = gnutls_init(&t->session, GNUTLS_SERVER | GNUTLS_NONBLOCK))) { + kr_log_error("[tls] gnutls_init() failed: %d (%s)\n", + err, gnutls_strerror_name(err)); + tls_free(t); + return NULL; + } + if ((err = gnutls_credentials_set(t->session, GNUTLS_CRD_CERTIFICATE, + t->x509_creds))) { + kr_log_error("[tls] warning: gnutls_credentials_set() failed: (%d) %s\n", + err, gnutls_strerror_name(err)); + } + if ((err = gnutls_priority_set_direct(t->session, priorities, &errpos))) { + kr_log_error("[tls] warning: setting priority '%s' failed at character %zd (...'%s') with error (%d) %s\n", + priorities, errpos - priorities, errpos, err, gnutls_strerror_name(err)); + } + gnutls_transport_set_pull_function(t->session, kres_gnutls_pull); + /* + * gnutls_transport_set_vec_push_function (t->session, + * kres_gnutls_push_vec); + */ + gnutls_transport_set_push_function(t->session, kres_gnutls_push); + gnutls_transport_set_ptr(t->session, t); + + return t; +} + +void +tls_free(struct tls_ctx_t *tls) +{ + if (!tls) { + return; + } + /* FIXME: do we want to do gnutls_bye() to close TLS cleanly ? */ + if (tls->session) { + gnutls_deinit(tls->session); + tls->session = NULL; + } + if (tls->x509_creds) { + gnutls_certificate_free_credentials(tls->x509_creds); + tls->x509_creds = NULL; + } + free(tls); +} + +int +push_tls(struct qr_task *task, uv_handle_t * handle, knot_pkt_t * pkt, + uv_write_t * writer, qr_task_send_cb on_send) +{ + ssize_t count; + if (!pkt) { + kr_log_error("[tls] cannot push null packet\n"); + return on_send(task, handle, kr_error(EIO)); + } + uint16_t pkt_size = htons(pkt->size); + + struct session *session = handle->data; + if (!session) { + kr_log_error("[tls] no session on push\n"); + return on_send(task, handle, kr_error(EIO)); + } + struct tls_ctx_t *tls_p = session->tls_ctx; + if (!tls_p) { + kr_log_error("[tls] no tls context on push\n"); + /* FIXME: might be necessary if we ever do outbound TLS */ + return on_send(task, handle, kr_error(EIO)); + } + tls_p->handle = (uv_stream_t *) handle; + tls_p->writer = writer; + gnutls_record_cork(tls_p->session); + count = gnutls_record_send(tls_p->session, &pkt_size, sizeof(pkt_size)); + if (count != sizeof(pkt_size)) { + kr_log_error("[tls] gnutls_record_send pkt_size fail wanted: %u (%zd) %s\n", + pkt_size, count, gnutls_strerror_name(count)); + return on_send(task, handle, kr_error(EIO)); + } + count = gnutls_record_send(tls_p->session, pkt->wire, pkt->size); + if (count != pkt->size) { + kr_log_error("[tls] gnutls_record_send wire fail wanted: %zu (%zd) %s\n", + pkt->size, count, gnutls_strerror_name(count)); + return on_send(task, handle, kr_error(EIO)); + } + count = gnutls_record_uncork(tls_p->session, 0); + if (count != sizeof(pkt_size) + pkt->size) { + if (count == GNUTLS_E_AGAIN || count == GNUTLS_E_INTERRUPTED) { + kr_log_error("[tls] gnutls_record_send incomplete: %zu (%zd) %s\n", + pkt->size, count, gnutls_strerror_name(count)); + /* + * FIXME: we need to know when this frees up; when it + * does, we should do gnutls_record_send(tls.session, + * NULL, 0); how do i know? + */ + } else { + kr_log_error("[tls] gnutls_record_send wire fail wanted: %zu (%zd) %s\n", + pkt->size, count, gnutls_strerror_name(count)); + } + return on_send(task, handle, kr_error(EIO)); + } + return count; +} + + +int +worker_process_tls(struct worker_ctx *worker, uv_stream_t * handle, const uint8_t * buf, ssize_t nread) +{ + struct session *session = handle->data; + struct tls_ctx_t *tls_p = session->tls_ctx; + if (!tls_p) { + return kr_error(ENOSYS); + } + int err; + ssize_t count; + + tls_p->buf = buf; + tls_p->nread = nread; + tls_p->handle = handle; + tls_p->consumed = 0; /* FIXME: doesn't handle split TLS records */ + if (!tls_p->handshake_done) { + kr_log_error("[tls] handshake not done, what is going on?\n"); + err = gnutls_handshake(tls_p->session); + if (!err) { + tls_p->handshake_done = 1; + } else { + kr_log_error("[tls] gnutls handshake gets: %d (%s)\n", err, gnutls_strerror_name(err)); + if (err == GNUTLS_E_AGAIN || err == GNUTLS_E_INTERRUPTED) { + return 0; /* Wait for more */ + } + return kr_error(err); + } + } + count = gnutls_record_recv(tls_p->session, tls_p->recv_buf, sizeof(tls_p->recv_buf)); + if (count == 0) { + /* this means there has been an end of the stream */ + kr_log_error("[tls] got zero from gnutls_record_recv\n"); + worker_submit(worker, (uv_handle_t *) handle, NULL, NULL); + return kr_error(EIO); + } + if (count < 0) { + if (count == GNUTLS_E_AGAIN || count == GNUTLS_E_INTERRUPTED) { + return 0; /* Wait for more */ + } else { + kr_log_error("[tls] unknown gnutls_record_recv error: (%zd) %s\n", + count, gnutls_strerror_name(count)); + worker_submit(worker, (uv_handle_t *) handle, NULL, NULL); + return kr_error(EIO); + } + } + kr_log_error("[tls] we got %zd cleartext octets\n", count); + return worker_process_tcp(worker, handle, tls_p->recv_buf, count); +} + +#undef DEBUG_MSG diff --git a/daemon/tls.h b/daemon/tls.h new file mode 100644 index 000000000..14646272b --- /dev/null +++ b/daemon/tls.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2016 American Civil Liberties Union (ACLU) + + 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 . +*/ + +#pragma once + +#include +#include + +struct tls_ctx_t; + +struct tls_ctx_t* tls_new(struct worker_ctx *worker); +void tls_free(struct tls_ctx_t* tls); + +int push_tls(struct qr_task *task, uv_handle_t *handle, knot_pkt_t *pkt, + uv_write_t *writer, qr_task_send_cb on_send); + +int worker_process_tls(struct worker_ctx *worker, uv_stream_t *handle, const uint8_t *buf, ssize_t nread); diff --git a/daemon/worker.c b/daemon/worker.c index 9e64d0df9..3a5380f11 100644 --- a/daemon/worker.c +++ b/daemon/worker.c @@ -30,6 +30,7 @@ #include "daemon/worker.h" #include "daemon/engine.h" #include "daemon/io.h" +#include "daemon/tls.h" /* @internal Union of various libuv objects for freelist. */ struct req @@ -455,13 +456,18 @@ static int qr_task_send(struct qr_task *task, uv_handle_t *handle, struct sockad send_req->as.send.data = task; ret = uv_udp_send(&send_req->as.send, (uv_udp_t *)handle, &buf, 1, addr, &on_send); } else { - uint16_t pkt_size = htons(pkt->size); - uv_buf_t buf[2] = { - { (char *)&pkt_size, sizeof(pkt_size) }, - { (char *)pkt->wire, pkt->size } - }; - send_req->as.write.data = task; - ret = uv_write(&send_req->as.write, (uv_stream_t *)handle, buf, 2, &on_write); + struct session *session = handle->data; + if (session->has_tls) { + ret = push_tls(task, handle, pkt, &send_req->as.write, qr_task_on_send); + } else { + uint16_t pkt_size = htons(pkt->size); + uv_buf_t buf[2] = { + { (char *)&pkt_size, sizeof(pkt_size) }, + { (char *)pkt->wire, pkt->size } + }; + send_req->as.write.data = task; + ret = uv_write(&send_req->as.write, (uv_stream_t *)handle, buf, 2, &on_write); + } } if (ret == 0) { qr_task_ref(task); /* Pending ioreq on current task */ @@ -834,7 +840,7 @@ int worker_end_tcp(struct worker_ctx *worker, uv_handle_t *handle) return 0; } -int worker_process_tcp(struct worker_ctx *worker, uv_handle_t *handle, const uint8_t *msg, ssize_t len) +int worker_process_tcp(struct worker_ctx *worker, uv_stream_t *handle, const uint8_t *msg, ssize_t len) { if (!worker || !handle) { return kr_error(EINVAL); @@ -865,7 +871,7 @@ int worker_process_tcp(struct worker_ctx *worker, uv_handle_t *handle, const uin * to buffer incoming message until it's complete. */ if (!session->outgoing) { if (!task) { - task = qr_task_create(worker, handle, NULL); + task = qr_task_create(worker, (uv_handle_t *)handle, NULL); if (!task) { return kr_error(ENOMEM); } diff --git a/daemon/worker.h b/daemon/worker.h index 98c083680..349a4d7da 100644 --- a/daemon/worker.h +++ b/daemon/worker.h @@ -108,7 +108,7 @@ int worker_submit(struct worker_ctx *worker, uv_handle_t *handle, knot_pkt_t *qu * If the fragment contains a complete query or completes current fragment, execute it. * @return 0 or an error code */ -int worker_process_tcp(struct worker_ctx *worker, uv_handle_t *handle, const uint8_t *msg, ssize_t len); +int worker_process_tcp(struct worker_ctx *worker, uv_stream_t *handle, const uint8_t *msg, ssize_t len); /** * End current DNS/TCP session, this disassociates pending tasks from this session @@ -128,3 +128,6 @@ int worker_reserve(struct worker_ctx *worker, size_t ring_maxlen); /** Collect worker mempools */ void worker_reclaim(struct worker_ctx *worker); + +struct qr_task; +typedef int (*qr_task_send_cb)(struct qr_task *task, uv_handle_t *handle, int status);