]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
WIP: first pass at TLS implementation
authorDaniel Kahn Gillmor <dkg@fifthhorseman.net>
Sun, 3 Apr 2016 13:13:24 +0000 (10:13 -0300)
committerOndřej Surý <ondrej@sury.org>
Fri, 5 Aug 2016 09:47:14 +0000 (11:47 +0200)
daemon/daemon.mk
daemon/io.c
daemon/io.h
daemon/tls.c [new file with mode: 0644]
daemon/tls.h [new file with mode: 0644]
daemon/worker.c
daemon/worker.h

index 6a3f1f90ed707d3d395bc73c6db253901f5ea59e..693e501c7e83a56d7f796050b1707eda00009a3c 100644 (file)
@@ -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
index 4058029fc76fbde974bc1aa0aa1afae286c1b1a2..1a69dbeeb012143c85fc0c312522465217613fd5 100644 (file)
@@ -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;
index ba29cf1e2ac20fabb3dba995b202bed2772705f2..a85df52dd78ccdd0b258952335fddf0a80720319 100644 (file)
@@ -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 (file)
index 0000000..ea6ecdb
--- /dev/null
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2016 American Civil Liberties Union (ACLU)
+ * 
+ * Initial Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
+ * 
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <gnutls/gnutls.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <uv.h>
+
+#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 (file)
index 0000000..1464627
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+*/
+
+#pragma once
+
+#include <uv.h>
+#include <libknot/packet/pkt.h>
+
+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);
index 9e64d0df93cadd60712586d7ed917ea15df7a7a1..3a5380f11c2b699c2800961d761161d0ba0beca7 100644 (file)
@@ -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);
                        }
index 98c083680e8c453ef138ccb858eebfcae2083405..349a4d7daeb9ae5f9f3492a9ab0b971de75b0848 100644 (file)
@@ -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);