diff options
30 files changed, 2664 insertions, 0 deletions
@@ -0,0 +1 @@ +defanor <defanor@uberspace.net> @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to <http://unlicense.org/> diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ChangeLog diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..a64b713 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,18 @@ +AM_CFLAGS = -Wall -Wextra -Werror -pedantic + +bin_PROGRAMS = tlsd fp2alias std2fifo + +tlsd_SOURCES = tlsd.c +tlsd_CFLAGS = $(LIBGNUTLS_CFLAGS) $(AM_CFLAGS) +tlsd_LDADD = $(LIBGNUTLS_LIBS) + +fp2alias_SOURCES = fp2alias.c +fp2alias_CFLAGS = $(AM_CFLAGS) + +std2fifo_SOURCES = std2fifo.c +std2fifo_CFLAGS = $(AM_CFLAGS) + +# docs +info_TEXINFOS = tlsd.texi +dist_man_MANS = tlsd.1 fp2alias.1 std2fifo.1 +EXTRA_DIST = examples @@ -0,0 +1,12 @@ + -*- outline -*- +This is the history of noteworthy changes in TLSd. + + +* Version 0.0.1 (2017-04-29) + Versioning is started. + +* 2017-04-21 + A P2P IM example is added. + +* 2017-04-12 + This NEWS file is created. @@ -0,0 +1,48 @@ +tlsd - a TLS daemon +=================== +TLSd is a daemon that both accepts and initiates TLS connections, runs +processes, and provides peer certificate's fingerprint as an +environment variable for them. The intent is to facilitate creation +and usage of simple services for peer-to-peer networking. + + +Examples +-------- +An echo server on a random port: + + $ tlsd -e cat + +Authentication: + + $ tlsd -p 5556 -- sh -c 'echo "Hello, ${SHA256}! I am a ${SIDE}."' + +Connection initiation: + + $ echo 'localhost 5600' | tlsd -e echo 'hello' + +Per-connection FIFO pairs: + + $ tlsd -p 5601 -e -- std2fifo -e ~/.chat/ + + +Installation +------------ +Dependencies: GnuTLS. To install from a tarball distribution: + + $ ./configure && make && make install + +It uses GNU Build System, so the standard options and targets are +present. + +Generated files are not included into the DVCS repository. To generate +them, use: + + $ aclocal && autoconf && autoheader && automake --add-missing + +X.509 certificates can be generated with GnuTLS's certtool or +OpenSSL's req command. + + +Documentation +------------- +An info manual and man pages are provided. diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..3ea3551 --- /dev/null +++ b/configure.ac @@ -0,0 +1,35 @@ +# -*- Autoconf -*- +# Process this file with autoconf to produce a configure script. + +AC_PREREQ([2.69]) +AC_INIT(TLSd, 0.0.1, defanor@uberspace.net, tlsd, + https://defanor.uberspace.net/projects/tlsd/) +AM_INIT_AUTOMAKE([-Wall]) +AC_CONFIG_SRCDIR([tlsd.c]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_FILES([Makefile]) + +# Checks for programs. +AC_PROG_CC +AM_PROG_CC_C_O + +# For pipe2(2) +AC_GNU_SOURCE + +# Checks for libraries. +PKG_CHECK_MODULES([LIBGNUTLS], [gnutls >= 3.3.0]) +AC_SUBST([LIBGNUTLS_CFLAGS]) +AC_SUBST([LIBGNUTLS_LIBS]) + +# Checks for header files. +AC_CHECK_HEADERS([arpa/inet.h netinet/in.h stdlib.h \ + string.h sys/socket.h syslog.h unistd.h]) + +# Checks for typedefs, structures, and compiler characteristics. +AC_TYPE_SIZE_T + +# Checks for library functions. +AC_FUNC_FORK +AC_CHECK_FUNCS([dup2 memset socket strerror]) + +AC_OUTPUT diff --git a/examples/chat/nc-chatroom.service b/examples/chat/nc-chatroom.service new file mode 100644 index 0000000..8c3f359 --- /dev/null +++ b/examples/chat/nc-chatroom.service @@ -0,0 +1,10 @@ +[Unit] +Description=Ncat chatroom + +[Service] +Type=simple +ExecStart=/usr/bin/nc -vl --broker 127.0.0.1 7000 +User=nobody + +[Install] +WantedBy=multi-user.target diff --git a/examples/chat/tls-chat.sh b/examples/chat/tls-chat.sh new file mode 100755 index 0000000..647331f --- /dev/null +++ b/examples/chat/tls-chat.sh @@ -0,0 +1,9 @@ +#!/bin/sh +rlwrap gnutls-cli --insecure --x509keyfile ~/.tls/key.pem \ + --x509certfile ~/.tls/cert.pem "${1}" --port="${2}" \ + | while read -r LINE +do echo "${LINE}" + case "${LINE}" in + *"${USER}"*) printf '\a' ;; + esac +done diff --git a/examples/chat/tlsd-chat.sh b/examples/chat/tlsd-chat.sh new file mode 100755 index 0000000..a1064a4 --- /dev/null +++ b/examples/chat/tlsd-chat.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +NAME="${ALIAS}[$$]" + +quit () { + echo "$(date -u +%R) * ${NAME} quits" | nc localhost 7000 +} +trap quit EXIT + +JOINMSG="$(date -u +%R) * ${NAME} joins" +echo "${JOINMSG}" +echo "${JOINMSG}" | nc localhost 7000 +while read -r LINE +do echo "$(date -u +%R) ${NAME}: ${LINE}" +done | stdbuf -oL tr -d '\000-\011\013-\037' | nc localhost 7000 diff --git a/examples/chat/tlsd-chatroom.service b/examples/chat/tlsd-chatroom.service new file mode 100644 index 0000000..3b80c42 --- /dev/null +++ b/examples/chat/tlsd-chatroom.service @@ -0,0 +1,13 @@ +[Unit] +Description=TLSd chatroom +Requires=nc-chatroom.service +After=syslog.target + +[Service] +Type=simple +Environment="PATH=/usr/local/bin/:/usr/bin/" +ExecStart=/usr/local/bin/tlsd -p 5600 -- fp2alias -a -- tlsd-chat.sh +User=tlsd + +[Install] +WantedBy=multi-user.target diff --git a/examples/file-server/accept-files.sh b/examples/file-server/accept-files.sh new file mode 100755 index 0000000..c3f472e --- /dev/null +++ b/examples/file-server/accept-files.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# Accept files from remote users. + +ROOT="/srv/tlsd/files" +USERDIR="${ROOT}/${ALIAS}" + +if read -r LINE +then if [ -d "${USERDIR}" ] + then cat > "${USERDIR}/$(echo "${LINE}" | sed -e 's/\.\.//g')" + else echo "No directory for you!" + fi +fi diff --git a/examples/file-server/browse-files.sh b/examples/file-server/browse-files.sh new file mode 100755 index 0000000..c93716d --- /dev/null +++ b/examples/file-server/browse-files.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Serves files and directory listings. + +ROOT="/srv/tlsd/files" + +while read -r LINE +do + echo "${SHA256}: ${LINE}" 1>&2 + FILEPATH="${ROOT}/$(echo "${LINE}" | sed -e 's/\.\.//g')" + if [ -d "${FILEPATH}" ] + then ls -l "${FILEPATH}" + elif [ -f "${FILEPATH}" ] + then cat "${FILEPATH}" + else echo "${FILEPATH} is neither a file nor a directory"; + fi +done diff --git a/examples/file-server/serve-files.sh b/examples/file-server/serve-files.sh new file mode 100755 index 0000000..5728c10 --- /dev/null +++ b/examples/file-server/serve-files.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# Serve a single file and exits. + +ROOT="/srv/tlsd/files" + +if read -r LINE +then echo "${SHA256}: ${LINE}" 1>&2 + FILEPATH="${ROOT}/$(echo "${LINE}" | sed -e 's/\.\.//g')" + if [ -f "${FILEPATH}" ] + then cat "${FILEPATH}" + else echo "${FILEPATH} is not a file" + fi +fi diff --git a/examples/p2p-im/Makefile b/examples/p2p-im/Makefile new file mode 100644 index 0000000..72ca0f5 --- /dev/null +++ b/examples/p2p-im/Makefile @@ -0,0 +1,33 @@ +# This Makefile builds and installs the libpurple plugin. + +# Using -Wno-unused-parameter here, since libpurple predefines those +# parameters, and we don't always need them. + +# Not using -Werror, since on Debian 9 -pedantic points to an error in +# libpurple/certificate.h. + +# C99 is needed to initialize large structures with less boilerplate, +# and glib requires it anyway. GNU extensions are also handy, so using +# gnu99. + +CC = gcc +CFLAGS += -std=gnu99 -Wall -Wextra -Wno-unused-parameter -pedantic \ + -g -DPURPLE_PLUGINS -fPIC -DPIC -shared \ + `pkg-config --cflags purple glib-2.0` +LDLIBS += `pkg-config --libs purple glib-2.0` +PLUGIN_DIR = `pkg-config --variable=plugindir purple` +SOURCES = libpurple-fifo-plugin.c +PLUGIN_ID = prpl-defanor-fifo +TARGET = ${PLUGIN_ID}.so + +all: + ${CC} ${CFLAGS} ${SOURCES} ${LDLIBS} -o ${TARGET} + +install: + install ${TARGET} ${PLUGIN_DIR} + +uninstall: + rm -f ${PLUGIN_DIR}/${TARGET} + +clean: + rm -f ${TARGET} diff --git a/examples/p2p-im/approximate-setup.sh b/examples/p2p-im/approximate-setup.sh new file mode 100644 index 0000000..a0fef4d --- /dev/null +++ b/examples/p2p-im/approximate-setup.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Assuming that tlsd and the plugin are installed, the key and +# certificate are already generated, placed into /etc/tls/, and are +# accessible to the tlsd group. +install tlsd-im{,-cmd,-reconnect}.sh /usr/local/bin/ +useradd --system -G tlsd tlsd-im +gpasswd -a bitlbee tlsd-im +gpasswd -a $USER tlsd-im +mkdir -p /var/lib/tlsd-im/ +chown tlsd-im:tlsd-im /var/lib/tlsd-im/ +chmod g+w /var/lib/tlsd-im +install tlsd-im.service tlsd-im-reconnect.{service,timer} /etc/systemd/system/ +systemctl enable tlsd-im.service tlsd-im-reconnect.timer +systemctl start tlsd-im.service tlsd-im-reconnect.timer diff --git a/examples/p2p-im/libpurple-fifo-plugin.c b/examples/p2p-im/libpurple-fifo-plugin.c new file mode 100644 index 0000000..15d34a5 --- /dev/null +++ b/examples/p2p-im/libpurple-fifo-plugin.c @@ -0,0 +1,400 @@ +/* + libpurple-fifo-plugin, an example plugin that interacts with FIFOs + created with std2fifo or similar programs. + + In this whole example, there is plenty to improve, but it should + work for basic message transmission. + + This is free and unencumbered software released into the public + domain. +*/ + +#include <glib.h> + +#include "prpl.h" +#include "version.h" +#include "debug.h" +#include "cmds.h" +#include "accountopt.h" + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <string.h> +#include <dirent.h> +#include <unistd.h> +#include <errno.h> +#include <sys/inotify.h> +#include <time.h> + + +#define PLUGIN_ID "prpl-defanor-fifo" +#define PLUGIN_NAME "FIFOs-based IM" +#define PLUGIN_VERSION "0.1-dev" +#define PLUGIN_SUMMARY "Reads from and writes into FIFOs" +#define PLUGIN_DESCRIPTION "This is an example for the TLSd manual" + +#define MAX_PEERS 64 +#define MAX_DIRNAME_LEN 256 +#define MAX_BUF_SIZE 4096 +#define FIFO_OUT_MODE S_IRWXU | S_IRWXG | S_IWGRP + + +typedef struct { + int fd; + int wd; + guint handle; +} watch; + +typedef struct { + int input; + int output; + char dirname[MAX_DIRNAME_LEN]; + char *path; + size_t path_len; + guint handle; + PurpleConnection *gc; +} peer; + +typedef struct { + watch w; + peer p[MAX_PEERS]; +} conn_state; + + +static void new_dir (gpointer data, gint source, PurpleInputCondition cond); +static void incoming_msg (gpointer data, gint source, PurpleInputCondition cond); +static void fifo_close(PurpleConnection *gc); + +void peer_reset (peer *cp) +{ + cp->output = -1; + cp->input = -1; + cp->path = NULL; + cp->path_len = 0; + cp->handle = 0; +} + +void peer_terminate (peer *cp) +{ + if (cp->path == NULL) + return; + purple_debug_misc(PLUGIN_ID, "Terminating %s\n", cp->dirname); + purple_prpl_got_user_status(cp->gc->account, cp->dirname, "offline", NULL); + if (cp->handle) + purple_input_remove (cp->handle); + if (cp->path != NULL) + free (cp->path); + if (cp->output != -1) + close (cp->output); + if (cp->input != -1) + close (cp->input); + peer_reset(cp); +} + +static int peer_suspend (peer *cp) +{ + purple_debug_misc(PLUGIN_ID, "Suspending %s\n", cp->dirname); + purple_prpl_got_user_status(cp->gc->account, cp->dirname, "offline", NULL); + if (cp->handle) + purple_input_remove (cp->handle); + if (cp->output != -1) + close (cp->output); + if (cp->input != -1) { + close (cp->input); + cp->input = -1; + } + strcpy(cp->path + cp->path_len - 4, "out"); + cp->output = open(cp->path, O_RDONLY | O_NONBLOCK); + if (cp->output == -1) { + purple_debug_error(PLUGIN_ID, "Failed to open %s: %s\n", + cp->path, strerror(errno)); + peer_terminate(cp); + return -1; + } + cp->handle = purple_input_add(cp->output, PURPLE_INPUT_READ, + incoming_msg, cp); + return 0; +} + + +static void incoming_msg (gpointer data, + gint source, + PurpleInputCondition cond) +{ + peer *cp = data; + ssize_t len; + static char buf[MAX_BUF_SIZE + 1]; + len = read(cp->output, buf, MAX_BUF_SIZE); + if (len < 0) { + /* Error */ + purple_debug_error(PLUGIN_ID, "Failed to read from %s: %s\n", + cp->dirname, strerror(errno)); + peer_terminate(cp); + return; + } else if (len == 0) { + /* EOF */ + purple_debug_misc(PLUGIN_ID, "EOF from %s\n", cp->dirname); + peer_suspend(cp); + return; + } + purple_prpl_got_user_status(cp->gc->account, cp->dirname, "available", NULL); + buf[len] = 0; + /* Messages would normally end with newline, but IM clients add + newlines on output as well, so we'll have to get rid of that. */ + if (buf[len - 1] == '\n') { + if (len == 1) + /* Could be an automatically added newline, ignore that. */ + return; + buf[len -1] = 0; + } + serv_got_im(purple_account_get_connection(cp->gc->account), + cp->dirname, buf, 0, time(NULL)); +} + +static int add_peer (PurpleConnection *gc, + const char *dname) +{ + conn_state *cs = gc->proto_data; + unsigned int i; + purple_debug_misc(PLUGIN_ID, "Adding peer %s\n", dname); + for (i = 0; (i < MAX_PEERS) && (cs->p[i].path != NULL); i++); + if (i == MAX_PEERS) { + purple_debug_error(PLUGIN_ID, "Too many peers (%d)\n", i); + return -1; + } + cs->p[i].gc = gc; + strncpy(cs->p[i].dirname, dname, MAX_DIRNAME_LEN - 1); + cs->p[i].path_len = + /* root, slash, sha256, slash, {in,out}, zero */ + strlen(gc->account->username) + 1 + strlen(dname) + 1 + 3 + 1; + cs->p[i].path = malloc(cs->p[i].path_len); + if (cs->p[i].path == NULL) { + purple_debug_error(PLUGIN_ID, "Failed to allocate %d bytes of memory\n", + (int) cs->p[i].path_len); + peer_terminate(&cs->p[i]); + return -1; + } + snprintf(cs->p[i].path, cs->p[i].path_len, + "%s/%s/out", gc->account->username, dname); + /* Create a FIFO if it doesn't exist yet. Might be nicer to just set + a watch and wait, but that'd be more cumbersome, so this will do + for now. */ + if (mkfifo(cs->p[i].path, FIFO_OUT_MODE) && errno != EEXIST) { + purple_debug_error(PLUGIN_ID, "Failed to create a FIFO at %s: %s\n", + cs->p[i].path, strerror(errno)); + peer_terminate(&cs->p[i]); + return -1; + } + cs->p[i].output = open(cs->p[i].path, O_RDONLY | O_NONBLOCK); + if (cs->p[i].output == -1) { + purple_debug_error(PLUGIN_ID, "Failed to open %s: %s\n", + cs->p[i].path, strerror(errno)); + peer_terminate(&cs->p[i]); + return -1; + } + strcpy(cs->p[i].path + cs->p[i].path_len - 4, "in"); + cs->p[i].input = open(cs->p[i].path, O_WRONLY | O_NONBLOCK); + purple_prpl_got_user_status(gc->account, cs->p[i].dirname, + (cs->p[i].input == -1) ? "offline" : "available", + NULL); + /* Set a callback */ + cs->p[i].handle = purple_input_add(cs->p[i].output, PURPLE_INPUT_READ, + incoming_msg, &cs->p[i]); + return i; +} + +static void new_dir (gpointer data, + gint source, + PurpleInputCondition cond) +{ + static char buf[sizeof(struct inotify_event) + NAME_MAX + 1]; + struct inotify_event *ie; + ssize_t len; + PurpleConnection *gc = data; + conn_state *cs = gc->proto_data; + purple_debug_misc(PLUGIN_ID, "A 'new dir' watch for %s has fired\n", + gc->account->username); + len = read (cs->w.fd, buf, sizeof(struct inotify_event) + NAME_MAX + 1); + if (len == 0) + return; + else if (len < 0) { + /* error */ + purple_debug_error(PLUGIN_ID, "A watch failure\n"); + fifo_close (gc); + return; + } + ie = (struct inotify_event *) &buf; + purple_debug_misc(PLUGIN_ID, "A watch for %s has fired: %s, read %d bytes\n", + gc->account->username, ie->name, (int) len); + add_peer (gc, ie->name); +} + + +static int load_peers (PurpleConnection *gc) +{ + DIR *dp; + struct dirent *ep; + + dp = opendir(gc->account->username); + if (dp == NULL) + return -1; + while ((ep = readdir(dp))) { + if ((ep->d_name[0] == '.') || (ep->d_type != DT_DIR)) + continue; + purple_debug_misc(PLUGIN_ID, "Loading %s\n", ep->d_name); + add_peer (gc, ep->d_name); + } + closedir(dp); + return 0; +} + + +static int fifo_send_im(PurpleConnection *gc, + const char *who, + const char *message, + PurpleMessageFlags flags) +{ + size_t written = 0, total = strlen(message), ret; + unsigned int i; + conn_state *cs = gc->proto_data; + /* Find peer */ + for (i = 0; i < MAX_PEERS; i++) { + if (! strncmp(who, cs->p[i].dirname, MAX_DIRNAME_LEN)) + break; + } + if (i == MAX_PEERS) + return -ENOTCONN; + /* Write */ + if (cs->p[i].input == -1) { + strcpy(cs->p[i].path + cs->p[i].path_len - 4, "in"); + cs->p[i].input = open(cs->p[i].path, O_WRONLY | O_NONBLOCK); + if (cs->p[i].input == -1) { + purple_debug_misc(PLUGIN_ID, "Not connected to %s\n", cs->p[i].dirname); + peer_suspend(&cs->p[i]); + return -ENOTCONN; + } + } + while (written < total) { + ret = write(cs->p[i].input, message + written, total - written); + if (ret <= 0) { + purple_debug_misc(PLUGIN_ID, "Failed writing to %s\n", cs->p[i].dirname); + peer_suspend(&cs->p[i]); + return -ENOTCONN; + } else { + written += ret; + } + } + write(cs->p[i].input, "\n", 1); + purple_prpl_got_user_status(gc->account, cs->p[i].dirname, "available", NULL); + return 1; +} + + +/* Login: take dir name from username, get subdirs, set add input + handlers from "out", watch directory with inotify and add an input + handler for that too. */ +static void fifo_login(PurpleAccount *acct) +{ + PurpleConnection *gc = purple_account_get_connection(acct); + conn_state *cs; + unsigned int i; + purple_debug_misc(PLUGIN_ID, "Login: %s\n", acct->username); + purple_connection_update_progress(gc, "Connecting", 0, 2); + /* Allocate and prepare connection state */ + cs = malloc(sizeof(conn_state)); + gc->proto_data = cs; + for (i = 0; i < MAX_PEERS; i++) + peer_reset(&cs->p[i]); + /* Add a watch */ + /* TODO: checks, error handling */ + cs->w.fd = inotify_init(); + cs->w.wd = inotify_add_watch(cs->w.fd, acct->username, IN_CREATE); + cs->w.handle = purple_input_add(cs->w.fd, PURPLE_INPUT_READ, + new_dir, gc); + /* Load peers */ + purple_debug_misc(PLUGIN_ID, "Loading peers\n"); + load_peers (gc); + purple_connection_update_progress(gc, "Connected", 1, 2); + purple_connection_set_state(gc, PURPLE_CONNECTED); +} + +static void fifo_close(PurpleConnection *gc) +{ + unsigned int i; + conn_state *cs = gc->proto_data; + /* Terminate the watch */ + purple_debug_misc(PLUGIN_ID, "Removing the watch\n"); + if (cs->w.handle) + purple_input_remove (cs->w.handle); + if (cs->w.fd != -1 && cs->w.wd != -1) + inotify_rm_watch (cs->w.fd, cs->w.wd); + if (cs->w.fd != -1) + close (cs->w.fd); + /* Terminate the peers */ + purple_debug_misc(PLUGIN_ID, "Terminating peers\n"); + for (i = 0; i < MAX_PEERS; i++) + peer_terminate(&cs->p[i]); + /* Free */ + free (gc->proto_data); + purple_debug_misc(PLUGIN_ID, "Closed: %s\n", gc->account->username); +} + + +static void fifo_init(PurplePlugin *plugin) +{ + purple_debug_misc(PLUGIN_ID, "Initializing\n"); +} + +static void fifo_destroy(PurplePlugin *plugin) +{ + purple_debug_misc(PLUGIN_ID, "Shutting down\n"); +} + +static const char *fifo_list_icon(PurpleAccount *acct, + PurpleBuddy *buddy) +{ + /* TODO: do something about it. Maybe draw an icon. */ + return "irc"; +} + +static GList *fifo_status_types(PurpleAccount *acct) +{ + GList *types = NULL; + types = g_list_prepend(types, purple_status_type_new(PURPLE_STATUS_AVAILABLE, + NULL, NULL, TRUE)); + types = g_list_prepend(types, purple_status_type_new(PURPLE_STATUS_OFFLINE, + NULL, NULL, TRUE)); + return types; +} + +static PurplePluginProtocolInfo fifo_info = + { + .options = OPT_PROTO_NO_PASSWORD, + .icon_spec = NO_BUDDY_ICONS, + .list_icon = fifo_list_icon, + .status_types = fifo_status_types, + .login = fifo_login, + .close = fifo_close, + .send_im = fifo_send_im, + .struct_size = sizeof(PurplePluginProtocolInfo) + }; + +static PurplePluginInfo info = + { + .magic = PURPLE_PLUGIN_MAGIC, + .major_version = PURPLE_MAJOR_VERSION, + .minor_version = PURPLE_MINOR_VERSION, + .type = PURPLE_PLUGIN_PROTOCOL, + .priority = PURPLE_PRIORITY_DEFAULT, + .id = PLUGIN_ID, + .name = PLUGIN_NAME, + .version = PLUGIN_VERSION, + .summary = PLUGIN_SUMMARY, + .description = PLUGIN_DESCRIPTION, + .destroy = fifo_destroy, + .extra_info = &fifo_info + }; + +PURPLE_INIT_PLUGIN(fifo, fifo_init, info) diff --git a/examples/p2p-im/tlsd-im-cmd.sh b/examples/p2p-im/tlsd-im-cmd.sh new file mode 100644 index 0000000..2888a66 --- /dev/null +++ b/examples/p2p-im/tlsd-im-cmd.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +create_fifo () { + if [ ! -e "${CHATDIR}${SHA256}/$1" ] + then mkfifo -m 660 "${CHATDIR}${SHA256}/$1" + fi +} + +CHATDIR="/var/lib/tlsd-im/" +mkdir -m 770 -p "${CHATDIR}${SHA256}/" +create_fifo "in" +create_fifo "out" +flock -n "${CHATDIR}${SHA256}/lock" std2fifo -c "${CHATDIR}" diff --git a/examples/p2p-im/tlsd-im-reconnect.service b/examples/p2p-im/tlsd-im-reconnect.service new file mode 100644 index 0000000..e118614 --- /dev/null +++ b/examples/p2p-im/tlsd-im-reconnect.service @@ -0,0 +1,13 @@ +[Unit] +Description=TLSd P2P IM reconnect +Requisite=tlsd-im.service +After=syslog.target + +[Service] +Type=oneshot +Environment="PATH=/usr/local/bin/:/usr/bin/" +ExecStart=/usr/local/bin/tlsd-im-reconnect.sh +User=tlsd-im + +[Install] +WantedBy=multi-user.target diff --git a/examples/p2p-im/tlsd-im-reconnect.sh b/examples/p2p-im/tlsd-im-reconnect.sh new file mode 100644 index 0000000..3fc149f --- /dev/null +++ b/examples/p2p-im/tlsd-im-reconnect.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +CHATDIR="/var/lib/tlsd-im/" + +for DIR in $(find "${CHATDIR}" -maxdepth 1 -mindepth 1 -type d) +do if [ -f "${DIR}/address" ] + then flock -n "${DIR}/lock" cat "${DIR}/address" > "${CHATDIR}connect" + fi +done diff --git a/examples/p2p-im/tlsd-im-reconnect.timer b/examples/p2p-im/tlsd-im-reconnect.timer new file mode 100644 index 0000000..e55e206 --- /dev/null +++ b/examples/p2p-im/tlsd-im-reconnect.timer @@ -0,0 +1,8 @@ +[Unit] +Description=TLSd P2P IM reconnect timer + +[Timer] +OnUnitInactiveSec=600 + +[Install] +WantedBy=timers.target diff --git a/examples/p2p-im/tlsd-im.service b/examples/p2p-im/tlsd-im.service new file mode 100644 index 0000000..254fc6d --- /dev/null +++ b/examples/p2p-im/tlsd-im.service @@ -0,0 +1,12 @@ +[Unit] +Description=TLSd P2P IM +Requires=nc-chatroom.service +After=syslog.target + +[Service] +Environment="PATH=/usr/local/bin/:/usr/bin/" +ExecStart=/usr/local/bin/tlsd-im.sh +User=tlsd-im + +[Install] +WantedBy=multi-user.target diff --git a/examples/p2p-im/tlsd-im.sh b/examples/p2p-im/tlsd-im.sh new file mode 100644 index 0000000..7c580ae --- /dev/null +++ b/examples/p2p-im/tlsd-im.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +CHATDIR="/var/lib/tlsd-im/" +mkdir -p "${CHATDIR}" + +if [ ! -e "${CHATDIR}connect" ] +then echo foo > /tmp/bar + mkfifo "${CHATDIR}connect" +fi + +tail -f "${CHATDIR}connect" | torify tlsd -p 18765 -- tlsd-im-cmd.sh diff --git a/fp2alias.1 b/fp2alias.1 new file mode 100644 index 0000000..8253bc9 --- /dev/null +++ b/fp2alias.1 @@ -0,0 +1,48 @@ +.TH fp2alias 1 + +.SH NAME +fp2alias - a basic fingerprint-to-alias converter + +.SH SYNOPSIS +fp2alias [\fIoption ...\fR] [--] [<\fIcommand\fR> [\fIargument ...\fR]] + +.SH DESCRIPTION +fp2alias is a helper program for TLSd. It reads the \fBSHA256\fR +environment variable, adds the \fBALIAS\fR variable by looking it up +in a file, and runs a given command with that variable in the +environment. + +If it's not allowed to add new aliases, it would reject unknown users. + +.SH OPTIONS +.IP "\fB\-f\fR \fIfile\fR" +A file with "\fIfingerprint\fR \fIalias\fR" entries (default is +\fB/etc/tls/aliases\fR). +.IP \fB\-a\fR +Add new aliases. +.IP "\fB\-i\fR \fIident\fR" +Syslog identifier to use. +.IP \fB\-e\fR +Print messages into stderr, in addition to syslog. +.IP \fB\-h\fR +Print a help message and exit. + +.SH EXAMPLES +.SS Echo server +.nf +tlsd -- fp2alias -- cat +.fi + +.SS Authentication +.nf +tlsd -- fp2alias -- sh -c 'echo "Hello, ${ALIAS}!"' +.fi + +.SH COPYING +This is free and unencumbered software released into the public +domain. + +.SH SEE ALSO +\fBtlsd\fR(1) + +See \fBinfo tlsd\fR for more documentation. diff --git a/fp2alias.c b/fp2alias.c new file mode 100644 index 0000000..b1f68d6 --- /dev/null +++ b/fp2alias.c @@ -0,0 +1,144 @@ +/* + fp2alias, a basic authentication and authorization helper + + This is free and unencumbered software released into the public + domain. +*/ + +#include <config.h> + +#include <syslog.h> +#include <stdio.h> +#include <unistd.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +/* This value is used in format strings as well */ +#define MAX_ALIAS_LEN 32 + +#define DEFAULT_ALIAS_FILE "/etc/tls/aliases" + +/* Get an alias from a file, or from a user */ +int get_alias (const char *filename, + const char *fingerprint, + char *alias, + int add_new) +{ + FILE * fd; + char hash[65]; + char login[MAX_ALIAS_LEN + 1]; + int l; + + /* Try to find the fingerprint */ + fd = fopen(filename, "r"); + if (!fd) { + syslog(LOG_ERR, "Can't open %s for reading: %s (errno=%d)", + filename, strerror(errno), errno); + return -1; + } + do { + l = fscanf(fd, "%64[a-f0-9] %32[a-z0-9]\n", hash, login); + if (! strncmp(hash, fingerprint, 64)) { + fclose(fd); + strncpy(alias, login, MAX_ALIAS_LEN + 1); + return 0; + } + } while (l != EOF); + + /* In read-only mode, that's all */ + if (! add_new) { + fclose(fd); + puts("I don't recognize you."); + return -1; + } + + /* Ask for an alias to add */ + puts("Enter your alias, please."); + fflush(stdout); + l = scanf("%32[a-z0-9]", alias); + if (l == EOF || strlen(alias) < 2) { + fclose(fd); + return -1; + } + + /* Check that the alias is not taken */ + fd = freopen(filename, "a+", fd); + if (!fd) { + syslog(LOG_ERR, "Can't reopen %s for writing: %s (errno=%d)", + filename, strerror(errno), errno); + return -1; + } + do { + l = fscanf(fd, "%64[a-f0-9] %32[a-z0-9]\n", hash, login); + if (! strncmp(alias, login, MAX_ALIAS_LEN)) { + fclose(fd); + printf("The '%s' alias is taken already.", alias); + return -1; + } + } while (l != EOF); + + /* Everything appears to be fine; add it */ + fprintf(fd, "%s %s\n", fingerprint, alias); + fclose(fd); + return 0; +} + +void print_help (const char *name) +{ + printf("Usage: %s [option ...] [--] [<command> [argument ...]]\n", name); + puts("Options:"); + puts(" -f <file> a file with \"<fingerprint> <alias>\" entries"); + puts(" -a add new aliases"); + puts(" -i <ident> syslog ident to use"); + puts(" -e print messages into stderr, in addition to syslog"); + puts(" -h print this help message and exit"); +} + +int main (int argc, + char **argv) +{ + int c; + int syslog_options = 0; + char *ident = "fp2alias"; + char *alias_file = DEFAULT_ALIAS_FILE; + char *sha256; + char alias[MAX_ALIAS_LEN + 1]; + int add_new = 0; + + ident = argv[0]; + + while ((c = getopt (argc, argv, "f:ai:eh")) != -1) + switch (c) + { + case 'f': alias_file = optarg; break; + case 'a': add_new = 1; break; + case 'i': ident = optarg; break; + case 'e': syslog_options |= LOG_PERROR; break; + case 'h': print_help(argv[0]); return 0; + default: print_help(argv[0]); return 1; + } + + openlog(ident, syslog_options, LOG_USER); + + sha256 = getenv("SHA256"); + if (! sha256) { + syslog(LOG_ERR, "The SHA256 environment variable is not defined"); + closelog(); + return 1; + } + + if (get_alias(alias_file, sha256, alias, add_new) < 0) { + closelog(); + return 1; + } + syslog(LOG_INFO, "Identified %s as %s", sha256, alias); + closelog(); + + /* Run a program if one is specified */ + if (argc > optind) { + setenv("ALIAS", alias, 1); + return execvp(argv[optind], &argv[optind]); + } + return 0; +} diff --git a/std2fifo.1 b/std2fifo.1 new file mode 100644 index 0000000..9f51bd4 --- /dev/null +++ b/std2fifo.1 @@ -0,0 +1,49 @@ +.TH std2fifo 1 + +.SH NAME +std2fifo - a std{in,out} <-> <dir>/<env var>/{in,out} proxy + +.SH SYNOPSIS +std2fifo [\fIoption ...\fR] [--] <\fIdir\fR> + +.SH DESCRIPTION +std2fifo is a helper program for TLSd. Given a root directory and an +environment variable, it creates a "<\fIroot dir\fR>/<\fIenv var\fR>/" +directory, writes input into the "out" FIFO in that directory, and +prints the "in" FIFO output. + +Overall, it tries to be suitable for use with TLSd (or other +super-servers), and with common tools on the other end. + +.SH OPTIONS +.IP "\fB\-v\fR \fIvar\fR" +An environment variable name (default is \fBSHA256\fR). +.IP \fB\-c\fR +Continuous streams mode: do not reopen streams once they are closed, +and do not close the "out" stream after each message. It is intended +for applications such as file transfer, as opposed to textual +messaging. +.IP "\fB\-i\fR \fIident\fR" +Syslog identifier to use. +.IP \fB\-e\fR +Print messages into stderr, in addition to syslog. +.IP \fB\-h\fR +Print a help message and exit. + +.SH EXAMPLES +.nf +FOO=bar std2fifo -v FOO -rbe /tmp/ +.fi + +.nf +tlsd -p 5601 -e -- std2fifo -rbe ~/.chat/ +.fi + +.SH COPYING +This is free and unencumbered software released into the public +domain. + +.SH SEE ALSO +\fBtlsd\fR(1) + +See \fBinfo tlsd\fR for more documentation. diff --git a/std2fifo.c b/std2fifo.c new file mode 100644 index 0000000..a3848e9 --- /dev/null +++ b/std2fifo.c @@ -0,0 +1,222 @@ +/* + std2fifo, a std{in,out} <-> <dir>/<env var>/{in,out} proxy + + This is free and unencumbered software released into the public + domain. +*/ + +#include <config.h> + +#include <syslog.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <signal.h> + +#define MAX_BUF_SIZE 4096 +#define DIR_MODE S_IRWXU | S_IRWXG | S_IWGRP | S_IRGRP +#define FIFO_IN_MODE S_IRUSR | S_IWUSR | S_IWGRP +#define FIFO_OUT_MODE S_IRUSR | S_IWUSR | S_IRGRP + +#define max(x,y) ((x) > (y) ? (x) : (y)) + + +int chdir_err (const char *dir) +{ + if (chdir(dir)) { + syslog(LOG_ERR, "Can't change directory to %s: %s", dir, strerror(errno)); + return -1; + } + return 0; +} + +int fifo_open (const char *path, + mode_t mode, + int flag) +{ + if (mkfifo(path, mode) && errno != EEXIST) { + syslog(LOG_ERR, "Failed to create FIFO %s: %s", path, strerror(errno)); + return -1; + } + return open(path, flag); +} + +int write_all (int fd, + const char *buffer, + ssize_t count, + const char *err) +{ + ssize_t written, ret; + for (written = 0; written < count; written += ret) { + ret = write (fd, buffer + written, count - written); + if (! ret) { + syslog(LOG_ERR, "write() has returned 0"); + return -1; + } else if (ret < 0) { + if (err != NULL) + syslog(LOG_WARNING, "Failed to write to %s: %s", err, strerror(errno)); + return -1; + } + } + return 0; +} + +int run (const char *dir, + const char *val, + int continuous) +{ + int in = -1, out = -1; + fd_set rfds; + int select_val, max_fd; + ssize_t len; + static char buffer[MAX_BUF_SIZE + 1]; + + if (chdir_err(dir)) + return -1; + + /* Create the directory if it doesn't exist */ + if (mkdir(val, DIR_MODE) && errno != EEXIST) { + syslog(LOG_ERR, "Failed to create directory %s: %s", val, strerror(errno)); + return -1; + } + + if (chdir_err(val)) + return -1; + + in = fifo_open("in", FIFO_IN_MODE, O_RDONLY | O_NONBLOCK); + if (in == -1) + return -1; + + for (;;) { + FD_ZERO(&rfds); + FD_SET(in, &rfds); + FD_SET(STDIN_FILENO, &rfds); + max_fd = max(STDIN_FILENO, in); + + select_val = select(max_fd + 1, &rfds, NULL, NULL, NULL); + if (select_val == -1) { + /* error */ + syslog(LOG_ERR, "select() failure: %s", strerror(errno)); + break; + } else { + /* stdin to FIFO */ + if (FD_ISSET(STDIN_FILENO, &rfds)) { + len = read(STDIN_FILENO, buffer, MAX_BUF_SIZE); + if (len < 0) { + /* Error: quit */ + syslog(LOG_ERR, "Failed to read from stdin: %s", strerror(errno)); + break; + } else if (len == 0) { + /* EOF: quit, but without error */ + close(in); + return 0; + } + /* Open, write, close */ + /* Only open once in the continuous streams mode */ + if ((continuous && out == -1) || ! continuous) { + out = fifo_open("out", FIFO_OUT_MODE, O_WRONLY); + if (out == -1) { + syslog(LOG_ERR, "Failed to open 'out': %s", strerror(errno)); + break; + } + } + if (write_all(out, buffer, len, "FIFO")) + break; + /* Do not close in the continuous streams mode */ + if (! continuous) + close(out); + } + /* FIFO to stdout */ + if (FD_ISSET(in, &rfds)) { + len = read(in, buffer, MAX_BUF_SIZE); + if (len < 0) { + /* Error: quit */ + syslog(LOG_ERR, "Failed to read from FIFO: %s", strerror(errno)); + break; + } else if (len == 0) { + /* EOF: reopen or quit, unless in continuous streams mode */ + close(in); + if (continuous) + break; + in = fifo_open("in", FIFO_IN_MODE, O_RDONLY | O_NONBLOCK); + if (in == -1) { + syslog(LOG_ERR, "Failed to reopen 'in': %s", strerror(errno)); + break; + } + } else if (write_all(STDOUT_FILENO, buffer, len, "stdout")) + break; + } + } + } + + /* It's an error if we've got here */ + if (in >= 0) + close(in); + if (out >= 0) + close(out); + return -1; +} + +void print_help (const char *name) +{ + printf("Usage: %s [option ...] <dir>\n", name); + puts("Options:"); + puts(" -v <var> an environment variable name"); + puts(" -c continuous streams"); + puts(" -i <ident> syslog ident to use"); + puts(" -e print messages into stderr, in addition to syslog"); + puts(" -h print this help message and exit"); +} + +int main (int argc, + char **argv) +{ + int c; + char *ident = "std2fifo"; + int syslog_options = 0, continuous = 0; + char *var = "SHA256", *val, *dir; + int ret; + struct sigaction sigact; + + while ((c = getopt (argc, argv, "v:ci:eh")) != -1) + switch (c) + { + case 'v': var = optarg; break; + case 'c': continuous = 1; break; + case 'i': ident = optarg; break; + case 'e': syslog_options |= LOG_PERROR; break; + case 'h': print_help(argv[0]); return 0; + default: print_help(argv[0]); return 1; + } + + if (argc <= optind) { + print_help(argv[0]); + return 1; + } + dir = argv[optind]; + val = getenv(var); + if (! val) { + print_help(argv[0]); + return 1; + } + + /* Prepare */ + openlog(ident, syslog_options, LOG_USER); + + /* Ignore SIGPIPE */ + sigact.sa_handler = SIG_IGN; + sigact.sa_flags = 0; + sigaction(SIGPIPE, &sigact, NULL); + + /* Run */ + ret = run(dir, val, continuous); + + /* Done */ + closelog(); + return ret; +} @@ -0,0 +1,67 @@ +.TH tlsd 1 + +.SH NAME +tlsd - a TLS daemon + +.SH SYNOPSIS +tlsd [\fIoption ...\fR] [--] <\fIcommand\fR> [\fIargument ...\fR] + +.SH DESCRIPTION +TLSd is a daemon that both accepts and initiates TLS connections, runs +processes, and provides peer certificate's fingerprint as an +environment variable for them. The intent is to facilitate creation +and usage of simple services for peer-to-peer networking. + +.SH OPTIONS +.IP "\fB\-k\fR \fIkeyfile\fR" +Private key file to use (default is \fB/etc/tls/key.pem\fR). +.IP "\fB\-c\fR \fIcertfile\fR" +Certificate file to use (default is \fB/etc/tls/cert.pem\fR). +.IP "\fB\-p\fR \fIport\fR" +Port to listen on (default is to use a randomly selected one). +.IP "\fB\-b\fR \fIhost\fR" +Bind address (default is 0.0.0.0). +.IP "\fB\-s\fR \fIsigno\fR" +Send a signal to a child on termination. See \fBsignal\fR(7) for +signal numbers. +.IP \fB\-n\fR +Do not require a peer certificate. This makes the \fBSHA256\fR +environment variable for child processes optional. +.IP "\fB-d\fR \fIdirectory\fR" +Write peer certificates in DER format into a directory. +.IP "\fB\-i\fR \fIident\fR" +Syslog identifier to use. +.IP \fB\-e\fR +Print messages into stderr, in addition to syslog. +.IP \fB\-h\fR +Print a help message and exit. + +.SH EXAMPLES +.SS Echo server +.nf +tlsd -e cat +.fi +.SS Authentication +.nf +tlsd -p 5556 -- sh -c 'echo "Hello, ${SHA256}! I am a ${SIDE}."' +.fi +.SS Connection initiation +.nf +echo 'localhost 5600' | tlsd -e echo 'hello' +.fi + +.SH SIGNALS +.IP "SIGINT, SIGTERM" +Terminate gracefully. + +.IP SIGHUP +Reload key and certificate. + +.SH COPYING +This is free and unencumbered software released into the public +domain. + +.SH SEE ALSO +\fBfp2alias\fR(1), \fBstd2fifo\fR(1) + +See \fBinfo tlsd\fR for more documentation. @@ -0,0 +1,817 @@ +/* + TLSd, a TLS daemon. + + This is free and unencumbered software released into the public + domain. +*/ + +#include <config.h> + +#include <fcntl.h> +#include <sys/wait.h> +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <arpa/inet.h> +#include <netinet/in.h> +#include <string.h> +#include <unistd.h> +#include <assert.h> +#include <netdb.h> +#include <syslog.h> +#include <gnutls/gnutls.h> + +#define MAX_BUF_SIZE 4096 +#define MAX_PEERS 64 +#define MAX_FINGERPRINT_BITS 256 +#define MAX_FINGERPRINT_NIBBLES MAX_FINGERPRINT_BITS / 4 +#define MAX_FINGERPRINT_BYTES MAX_FINGERPRINT_BITS / 8 +#define FINGERPRINT_HASH "SHA256" +#define DEFAULT_PORT "0" +#define DEFAULT_HOST "0.0.0.0" +#define DEFAULT_KEYFILE "/etc/tls/key.pem" +#define DEFAULT_CERTFILE "/etc/tls/cert.pem" + +#define max(x,y) ((x) > (y) ? (x) : (y)) +#define cc c[ci] +#define ATLS(x) assert(x == GNUTLS_E_SUCCESS) + + +/* Program options */ +typedef struct { + char *port; + char *host; + char *keyfile; + char *certfile; + char **args; + int child_kill_signo; + gnutls_certificate_request_t cert_req; + char *peer_cert_path; + size_t peer_cert_dir_len; +} options; + +/* Child process information */ +typedef struct { + int input; + int output; + pid_t pid; +} child_proc; + +/* Connection slot */ +typedef struct { + /* TODO: consider adding an "active" flag. */ + int tcp_socket; + gnutls_session_t tls_session; + child_proc child; + int side; +} conn; + +static gnutls_dh_params_t dh_params; +static int reload; + +/* Print an error message, along with errno. */ +void err_msg (int p, + const char *str) +{ + syslog(p, "%s: %s (errno=%d)", str, strerror(errno), errno); +} + +static int generate_dh_params (void) +{ + unsigned int bits = + gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_HIGH); + if (bits == 0) + return -1; + ATLS(gnutls_dh_params_init(&dh_params)); + ATLS(gnutls_dh_params_generate2(dh_params, bits)); + return 0; +} + +/* Get the fingerprint, print it into a string, and write the + certificate into a file if needed. */ +int read_peer_cert (gnutls_session_t session, + char *str, + size_t *str_len, + char *cert_path, + size_t cert_dir_len) +{ + const gnutls_datum_t *peers_cert; + size_t raw_len = MAX_FINGERPRINT_BYTES; + static unsigned char raw[MAX_FINGERPRINT_BYTES]; + unsigned int list_size = 0; + size_t i; + int ret; + FILE *fs; + size_t written; + + peers_cert = gnutls_certificate_get_peers(session, &list_size); + if (peers_cert == NULL) + return -1; + ret = gnutls_fingerprint(gnutls_digest_get_id(FINGERPRINT_HASH), + peers_cert, + raw, + &raw_len); + if (ret < 0) + return ret; + for (i = 0; i < raw_len; i++) + snprintf(str + i * 2, 3, "%02x", raw[i]); + *str_len = raw_len * 2; + + if (cert_path) { + strncpy(cert_path + cert_dir_len, str, MAX_FINGERPRINT_NIBBLES); + fs = fopen(cert_path, "wx"); + if (!fs) { + if (errno == EEXIST) + return 0; + syslog(LOG_ERR, "Can't open %s for writing: %s (errno=%d)", + cert_path, strerror(errno), errno); + return -1; + } + cert_path[cert_dir_len + MAX_FINGERPRINT_NIBBLES] = 0; + written = fwrite(peers_cert->data, 1, peers_cert->size, fs); + if (written != peers_cert->size) + syslog(LOG_ERR, "Failed to write a peer certificate into %s", + cert_path); + if (fclose(fs)) + syslog(LOG_ERR, "Failed to close %s: %s (errno=%d)", + cert_path, strerror(errno), errno); + } + return 0; +} + +/* Run a child process, make its stdin and stdout available via + child.{input,output}. */ +int run_child (conn *c, + options opt) +{ + pid_t pid; + int to_child[2], from_child[2]; + size_t fingerprint_len = MAX_FINGERPRINT_NIBBLES; + static char fingerprint_str[MAX_FINGERPRINT_NIBBLES + 1]; + + /* Set the side in environment */ + if (setenv("SIDE", c->side == GNUTLS_CLIENT ? "CLIENT" : "SERVER", 1)) { + err_msg(LOG_ERR, "Failed to set the SIDE environment variable"); + return -1; + } + if (unsetenv(FINGERPRINT_HASH)) { + err_msg(LOG_ERR, "Failed to unset the fingerprint environment variable"); + return -1; + } + + /* Read peer's certificate */ + if (read_peer_cert(c->tls_session, fingerprint_str, &fingerprint_len, + opt.peer_cert_path, opt.peer_cert_dir_len) < 0) + { + /* No fingerprint, but it may be fine */ + syslog(LOG_WARNING, "Unable to get a fingreprint string"); + if (opt.cert_req == GNUTLS_CERT_REQUIRE) { + syslog(LOG_ERR, "Peer certificate is required"); + return -1; + } + } + else { + /* Got a fingerprint; set it in environment */ + fingerprint_str[MAX_FINGERPRINT_NIBBLES] = 0; + syslog(LOG_DEBUG, "Peer's fingerprint: %s", fingerprint_str); + if (setenv(FINGERPRINT_HASH, fingerprint_str, 1)) { + err_msg(LOG_ERR, "Failed to set the fingerprint environment variable"); + return -1; + } + } + + /* Create pipes */ + if (pipe2(to_child, O_CLOEXEC) || pipe2(from_child, O_CLOEXEC)) { + syslog(LOG_ERR, "Failed to create pipes"); + return -1; + } + + /* Fork */ + pid = fork(); + if (pid < 0) { + /* Error */ + err_msg(LOG_ERR, "Failed to fork"); + return -1; + } else if (pid == 0) { + /* Child */ + if (dup2(to_child[0], STDIN_FILENO) < 0 + || dup2(from_child[1], STDOUT_FILENO) < 0) + { + syslog(LOG_ERR, "Failed to set standard I/O in child"); + exit(1); + } + execvp(opt.args[0], opt.args); + syslog(LOG_ERR, "Failed to execute %s: %s (errno=%d)", + opt.args[0], strerror(errno), errno); + exit(1); + } else { + /* Parent */ + if (close(to_child[0]) == -1 + || close(from_child[1])) + err_msg(LOG_ERR, "Failed to close pipes in parent"); + c->child.input = to_child[1]; + c->child.output = from_child[0]; + c->child.pid = pid; + return 0; + } +} + +/* Initialize a TLS connection */ +int tls_conn_init (gnutls_certificate_credentials_t x509_cred, + gnutls_certificate_request_t cert_req, + unsigned int side, + int sd, + conn *c) +{ + gnutls_session_t session; + int ret; + + /* Initialize structures */ + ATLS(gnutls_init(&session, side)); + ATLS(gnutls_set_default_priority(session)); + ATLS(gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, x509_cred)); + gnutls_certificate_server_set_request(session, cert_req); + gnutls_heartbeat_enable(session, GNUTLS_HB_PEER_ALLOWED_TO_SEND); + gnutls_transport_set_int(session, sd); + + /* Perform handshake */ + do { + ret = gnutls_handshake(session); + } while (ret < 0 && ! gnutls_error_is_fatal(ret)); + + if (ret < 0) { + syslog(LOG_WARNING, "TLS handshake has failed: %s (err=%d)", + gnutls_strerror(ret), ret); + if (shutdown(sd, SHUT_RDWR)) + err_msg(LOG_WARNING, "Failed to shutdown a TCP socket"); + if (close(sd)) + err_msg(LOG_WARNING, "Failed to close a TCP socket"); + gnutls_deinit(session); + return -1; + } + + /* Update the conn structure */ + c->tls_session = session; + c->tcp_socket = sd; + c->side = side; + return 0; +} + +/* Accept a new TLS connection. */ +int tls_accept (conn *c, + int listener, + gnutls_certificate_credentials_t x509_cred, + gnutls_certificate_request_t cert_req) +{ + static struct sockaddr sa; + socklen_t sa_len = sizeof(sa); + int sd; + int ret; + static char nhost[NI_MAXHOST], nserv[NI_MAXSERV]; + + /* Accept a TCP connection */ + sd = accept(listener, &sa, &sa_len); + if (sd == -1) { + err_msg(LOG_WARNING, "Failed to accept a TCP connection"); + return -1; + } + ret = getnameinfo(&sa, sa_len, nhost, sizeof(nhost), nserv, sizeof(nserv), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret) { + syslog(LOG_WARNING, "Accepted, but failed to get name info: %s (err=%d)", + gai_strerror(ret), ret); + if (close(sd)) + err_msg(LOG_WARNING, "Failed to close a TCP socket"); + return -1; + } + syslog(LOG_INFO, "Accepted a TCP connection from %s, port %s", + nhost, nserv); + return tls_conn_init(x509_cred, cert_req, GNUTLS_SERVER, sd, c); +} + +/* Initiate a new TCP connection */ +int tcp_connect (const char *host, + const char *service) +{ + int sd; + static struct addrinfo hints; + struct addrinfo *addr, *addrp; + int ret; + static char nhost[NI_MAXHOST], nserv[NI_MAXSERV]; + + /* Look up the address */ + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + syslog(LOG_DEBUG, "Resolving %s:%s", host, service); + ret = getaddrinfo(host, service, &hints, &addr); + if (ret) { + syslog(LOG_ERR, "Failed to get address information: %s (err=%d)", + gai_strerror(ret), ret); + return -1; + } + + /* Try each resolved address in order */ + for (addrp = addr; addrp != NULL; addrp = addrp->ai_next) { + /* Translate host and service into numeric names */ + ret = getnameinfo(addrp->ai_addr, addrp->ai_addrlen, + nhost, sizeof(nhost), nserv, sizeof(nserv), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret) { + syslog(LOG_WARNING, "Failed to get name information: %s (err=%d)", + gai_strerror(ret), ret); + continue; + } + /* Create a socket */ + sd = socket(addrp->ai_family, addrp->ai_socktype, addrp->ai_protocol); + if (sd == -1) { + err_msg(LOG_ERR, "Unable to create a socket"); + freeaddrinfo(addr); + return -1; + } + /* Attempt to connect */ + syslog(LOG_DEBUG, "Connecting to %s, port %s", nhost, nserv); + if (connect(sd, addrp->ai_addr, addrp->ai_addrlen)) { + err_msg(LOG_WARNING, "Connection failure"); + } else { + syslog(LOG_INFO, "Connected to %s, port %s", nhost, nserv); + freeaddrinfo(addr); + return sd; + } + /* Close the socket if we've got this far */ + if (close(sd)) + err_msg(LOG_WARNING, "Unable to close a socket"); + } + /* Give up: cleanup and report an error */ + syslog(LOG_ERR, "Unable to connect"); + freeaddrinfo(addr); + return -1; +} + +/* Initiate a new TLS connection */ +int tls_connect (conn *c, + gnutls_certificate_credentials_t x509_cred, + const char *host, + const char *service) +{ + int sd = tcp_connect(host, service); + if (sd < 0) + return -1; + return tls_conn_init(x509_cred, GNUTLS_CERT_REQUIRE, GNUTLS_CLIENT, sd, c); +} + +/* Create a socket, bind, listen, return it. */ +int tcp_listen (const char *host, + const char *service) +{ + int sd; + int optval = 1; + static struct addrinfo hints; + struct addrinfo *addr, *addrp; + int ret; + static char nhost[NI_MAXHOST], nserv[NI_MAXSERV]; + struct sockaddr sa; + socklen_t sa_len = sizeof(sa); + + /* Look up the address */ + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + ret = getaddrinfo(host, service, &hints, &addr); + if (ret) { + syslog(LOG_ERR, "Failed to get address information: %s (err=%d)", + gai_strerror(ret), ret); + return -1; + } + /* Try each resolved address in order */ + for (addrp = addr; addrp != NULL; addrp = addrp->ai_next) { + /* Translate host and service into numeric names */ + ret = getnameinfo(addrp->ai_addr, addrp->ai_addrlen, + nhost, sizeof(nhost), nserv, sizeof(nserv), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret) { + syslog(LOG_WARNING, "Failed to get name information: %s (err=%d)", + gai_strerror(ret), ret); + continue; + } + /* Create a socket */ + sd = socket(addrp->ai_family, addrp->ai_socktype, addrp->ai_protocol); + if (sd == -1) { + err_msg(LOG_ERR, "Unable to create a socket"); + freeaddrinfo(addr); + return -1; + } + /* Attempt to bind */ + syslog(LOG_DEBUG, "Attempting to bind: %s, port %s", nhost, nserv); + if (setsockopt (sd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(int)) + || bind(sd, addrp->ai_addr, addrp->ai_addrlen) + || listen(sd, MAX_PEERS) + || getsockname(sd, &sa, &sa_len)) + { + err_msg(LOG_WARNING, "Failed to bind and listen"); + } + else + { + /* Bound, find out where */ + ret = getnameinfo(&sa, sa_len, + nhost, sizeof(nhost), nserv, sizeof(nserv), + NI_NUMERICHOST | NI_NUMERICSERV); + if (ret) { + syslog(LOG_WARNING, + "Bound, but failed to get name information: %s (err=%d)", + gai_strerror(ret), ret); + continue; + } + /* Report, cleanup, return */ + syslog(LOG_INFO, "Listening on %s, port %s", nhost, nserv); + freeaddrinfo(addr); + return sd; + } + if (close(sd)) + err_msg(LOG_WARNING, "Unable to close a socket"); + } + + syslog(LOG_ERR, "Failed to bind"); + freeaddrinfo(addr); + return -1; +} + +/* Set conn's fds to -1. */ +void conn_reset (conn *c) +{ + c->tcp_socket = -1; + c->child.input = -1; + c->child.output = -1; +} + +/* Terminate a connection: close, disconnect, cleanup, etc. */ +int conn_terminate (conn *c, + int tls_close, + int child_kill_signo) +{ + int status; + /* TODO: maybe keep better track of what should be closed, and then + add warnings for when close() fails unexpectedly. */ + if (c->child.input >= 0 && close(c->child.input) < 0) + err_msg(LOG_ERR, "Failed to close child's stdin"); + if (c->child.output >= 0 && close(c->child.output) < 0) + err_msg(LOG_ERR, "Failed to close child's stdout"); + if (tls_close) + gnutls_bye(c->tls_session, GNUTLS_SHUT_WR); + gnutls_deinit(c->tls_session); + shutdown(c->tcp_socket, SHUT_RDWR); + if (c->tcp_socket >= 0 && close(c->tcp_socket) < 0) + err_msg(LOG_ERR, "Failed to close TCP socket"); + if (c->child.pid) { + if (kill(c->child.pid, child_kill_signo)) + err_msg (LOG_ERR, "Failed to send signal to a child"); + syslog(LOG_INFO, "Waiting for process %d to exit", c->child.pid); + if (waitpid(c->child.pid, &status, 0) < 1) + err_msg(LOG_ERR, "Error while waiting for a child to exit"); + else if (WIFEXITED(status)) + syslog(LOG_INFO, "Process %d has exited with status %d", + c->child.pid, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + syslog(LOG_NOTICE, "Process %d was terminated by signal %d", + c->child.pid, WTERMSIG(status)); + else + syslog(LOG_WARNING, "Process %d was terminated abnormally", c->child.pid); + } + conn_reset(c); + return 0; +} + +void on_signal (int signo) +{ + syslog(LOG_INFO, "Received signal %d", signo); + if (signo == SIGHUP) + reload = 1; +} + +/* Block some signals, but fill oldset in order to use them for + pselect later. Also set a dummy handler. */ +int set_signals (sigset_t *oldset) +{ + struct sigaction sigact; + sigset_t sigset; + + /* Block signals */ + if (sigemptyset(&sigset) + || sigaddset(&sigset, SIGTERM) + || sigaddset(&sigset, SIGINT) + || sigaddset(&sigset, SIGHUP) + || sigprocmask(SIG_BLOCK, &sigset, oldset)) + { + err_msg(LOG_ERR, "Unable to block signals"); + return -1; + } + + /* Handle signals */ + sigact.sa_handler = on_signal; + sigact.sa_flags = 0; + if (sigemptyset(&sigact.sa_mask) + || sigaction(SIGTERM, &sigact, NULL) + || sigaction(SIGINT, &sigact, NULL) + || sigaction(SIGHUP, &sigact, NULL) + || sigaction(SIGPIPE, &sigact, NULL)) + { + err_msg(LOG_ERR, "Unable to handle signals"); + return -1; + } + return 0; +} + +int cred_load (gnutls_certificate_credentials_t *x509_cred, + options opt) +{ + int ret; + ret = gnutls_certificate_allocate_credentials(x509_cred); + if (ret < 0) + syslog(LOG_ERR, "Failed to allocate credentials: %s (err=%d)", + gnutls_strerror(ret), ret); + ret = gnutls_certificate_set_x509_key_file(*x509_cred, opt.certfile, + opt.keyfile, GNUTLS_X509_FMT_PEM); + if (ret < 0) + syslog(LOG_ERR, "Failed to load key or certificate: %s (err=%d)", + gnutls_strerror(ret), ret); + gnutls_certificate_set_dh_params(*x509_cred, dh_params); + return ret; +} + +/* Run the server */ +/* TODO: consider splitting this function into a few smaller ones. */ +int serve (options opt) +{ + int listener; + int ret; + static gnutls_certificate_credentials_t x509_cred; + static char buffer[MAX_BUF_SIZE + 1]; + fd_set rfds; /* for pselect() */ + int select_val; /* pselect() return value */ + int max_fd; /* for pselect() */ + static conn c[MAX_PEERS]; /* peers */ + unsigned int ci, nci; /* peer index, next unused peer index */ + sigset_t sigmask; /* to use in pselect() */ + int sent, received; + /* A fixed-size r_format is not great, but perhaps better than + dynamic allocation. 64 bytes should be enough for everyone. */ + static char r_host[NI_MAXHOST], r_service[NI_MAXSERV], r_format[64]; + int stdin_eof = 0; + + /* Initialization */ + syslog(LOG_DEBUG, "Initializing"); + snprintf(r_format, sizeof(r_format), "%%%ds %%%ds", NI_MAXHOST, NI_MAXSERV); + ATLS(gnutls_global_init()); + assert(generate_dh_params() == 0); + if (cred_load(&x509_cred, opt)) + return -1; + for (ci = 0; ci < MAX_PEERS; ci++) + conn_reset(&cc); + if (set_signals(&sigmask) < 0) + return -1; + listener = tcp_listen(opt.host, opt.port); + if (listener < 0) + return -1; + + /* Event loop */ + for (;;) { + /* Point nci to the first unused peer slot */ + for (nci = 0; nci < MAX_PEERS && c[nci].tcp_socket >= 0; nci++); + + /* Select */ + FD_ZERO(&rfds); + if (nci < MAX_PEERS) { + /* Only accept or create new connections when there are free + slots */ + FD_SET(listener, &rfds); + if (! stdin_eof) + FD_SET(STDIN_FILENO, &rfds); + } + + for (max_fd = listener, ci = 0; ci < MAX_PEERS; ci++) { + if (cc.tcp_socket >= 0) { + FD_SET(cc.tcp_socket, &rfds); + max_fd = max(max_fd, cc.tcp_socket); + } + if (cc.child.output >= 0) { + FD_SET(cc.child.output, &rfds); + max_fd = max(max_fd, cc.child.output); + } + } + + select_val = pselect(max_fd + 1, &rfds, NULL, NULL, NULL, &sigmask); + + if (select_val == -1) { + /* Select error */ + if (errno == EINTR) { + if (reload) { + reload = 0; + syslog(LOG_INFO, "Reloading key and certificate"); + gnutls_certificate_free_credentials(x509_cred); + if (cred_load(&x509_cred, opt)) + break; + } else { + syslog(LOG_INFO, "Terminating gracefully"); + break; + } + } else { + err_msg(LOG_ERR, "pselect() failure"); + break; + } + } else if (select_val) { + /* New connection request */ + if (nci < MAX_PEERS && FD_ISSET(STDIN_FILENO, &rfds)) { + ret = scanf(r_format, r_host, r_service); + if (ret == 2) { + if (! tls_connect(&c[nci], x509_cred, r_host, r_service)) { + if (run_child(&c[nci], opt) < 0) { + syslog(LOG_ERR, "Failed to run a child process"); + conn_terminate(&c[nci], 1, opt.child_kill_signo); + } else { + /* Update nci */ + for (; nci < MAX_PEERS && c[nci].tcp_socket >= 0; nci++); + } + } + } else if (ret == EOF) { + syslog(LOG_INFO, "stdin is closed"); + stdin_eof = 1; + } else { + syslog(LOG_ERR, "Failed to scan host and port from stdin"); + stdin_eof = 1; + } + select_val--; + } + + /* New incoming connection */ + if (nci < MAX_PEERS && FD_ISSET(listener, &rfds)) { + if (! tls_accept(&c[nci], listener, x509_cred, opt.cert_req)) { + if (run_child(&c[nci], opt) < 0) { + syslog(LOG_ERR, "Failed to run a child process"); + conn_terminate(&c[nci], 1, opt.child_kill_signo); + } else { + /* Update nci */ + for (; nci < MAX_PEERS && c[nci].tcp_socket >= 0; nci++); + } + } + select_val--; + } + + /* Pass messages */ + for (ci = 0; ci < MAX_PEERS; ci++) { + /* TLS peer to child process */ + if (cc.tcp_socket >= 0 && FD_ISSET(cc.tcp_socket, &rfds)) { + do { + received = gnutls_record_recv(cc.tls_session, buffer, MAX_BUF_SIZE); + if (! received) { + syslog(LOG_DEBUG, "EOF from the TLS end"); + conn_terminate(&cc, 0, opt.child_kill_signo); + } else if (received < 0) { + syslog(LOG_WARNING, "Failed to receive: %s (err=%d)", + gnutls_strerror(received), received); + conn_terminate(&cc, 0, opt.child_kill_signo); + } else + for (sent = 0; sent < received; sent += ret) { + ret = write(cc.child.input, buffer + sent, received - sent); + if (! ret) { + syslog(LOG_ERR, "write() has returned 0"); + conn_terminate(&cc, 1, opt.child_kill_signo); + break; + } else if (ret < 0) { + err_msg(LOG_ERR, "Failed to write"); + conn_terminate(&cc, 1, opt.child_kill_signo); + break; + } + } + } while (cc.tcp_socket >= 0 + && gnutls_record_check_pending(cc.tls_session)); + select_val--; + } + /* Child process to TLS peer */ + if (cc.child.output >= 0 && FD_ISSET(cc.child.output, &rfds)) { + received = read(cc.child.output, buffer, MAX_BUF_SIZE); + if (! received) { + syslog(LOG_DEBUG, "EOF from the child process"); + conn_terminate(&cc, 1, opt.child_kill_signo); + } else if (received < 0) { + err_msg(LOG_WARNING, "Failed to read"); + conn_terminate(&cc, 1, opt.child_kill_signo); + } else + for (sent = 0; sent < received; sent += ret) { + ret = gnutls_record_send(cc.tls_session, + buffer + sent, + received - sent); + if (! ret) { + syslog(LOG_ERR, "gnutls_record_send() has returned 0"); + conn_terminate(&cc, 1, opt.child_kill_signo); + break; + } else if (ret < 0) { + syslog(LOG_ERR, "Failed to send: %s (err=%d)", + gnutls_strerror(ret), ret); + conn_terminate(&cc, 1, opt.child_kill_signo); + break; + } + } + select_val--; + } + } + if (select_val) + syslog(LOG_WARNING, "Not all the events are processed"); + /* TODO: maybe analyze the situation. */ + } + } + + /* Cleanup and close */ + for (ci = 0; ci < MAX_PEERS; ci++) { + if (cc.tcp_socket >= 0) + conn_terminate(&cc, 1, opt.child_kill_signo); + } + close(listener); + gnutls_certificate_free_credentials(x509_cred); + gnutls_dh_params_deinit(dh_params); + gnutls_global_deinit(); + syslog(LOG_INFO, "Shutting down"); + return 0; +} + +void print_help (const char *name) +{ + puts(PACKAGE_STRING); + printf("Usage: %s [option ...] [--] <command> [argument ...]\n", + name); + puts("Options:"); + puts(" -k <keyfile> private key file to use"); + puts(" -c <certfile> certificate file to use"); + puts(" -p <port> port to listen on"); + puts(" -b <host> bind address"); + puts(" -s <signo> a signal to send to a child on termination"); + puts(" -n do not require a peer certificate"); + puts(" -d <directory> write peer certificates into a directory"); + puts(" -i <ident> syslog ident to use"); + puts(" -e print messages into stderr, in addition to syslog"); + puts(" -h print this help message and exit"); +} + +/* Read options, run the serve function */ +int main (int argc, + char **argv) +{ + int c; + int ret; + int syslog_options = 0; + char *ident = "tlsd"; + char *peer_cert_dir = NULL; + options opt = { DEFAULT_PORT, DEFAULT_HOST, + DEFAULT_KEYFILE, DEFAULT_CERTFILE, + NULL, 0, GNUTLS_CERT_REQUIRE, NULL, 0 }; + + /* Parse the arguments */ + while ((c = getopt (argc, argv, "k:c:p:b:s:nd:i:eh")) != -1) + switch (c) + { + case 'k': opt.keyfile = optarg; break; + case 'c': opt.certfile = optarg; break; + case 'p': opt.port = optarg; break; + case 'b': opt.host = optarg; break; + case 's': opt.child_kill_signo = atoi(optarg); break; + case 'n': opt.cert_req = GNUTLS_CERT_REQUEST; break; + case 'd': peer_cert_dir = optarg; break; + case 'i': ident = optarg; break; + case 'e': syslog_options |= LOG_PERROR; break; + case 'h': print_help(argv[0]); return 0; + default: print_help(argv[0]); return 1; + } + + if (argc <= optind) { + print_help(argv[0]); + return 1; + } + opt.args = &argv[optind]; + + openlog(ident, syslog_options, LOG_USER); + if (peer_cert_dir) { + opt.peer_cert_dir_len = strlen(peer_cert_dir); + /* TODO: consider using chdir or open/openat instead */ + opt.peer_cert_path = malloc(opt.peer_cert_dir_len + + MAX_FINGERPRINT_NIBBLES + 1); + if (! opt.peer_cert_path) { + syslog(LOG_ERR, "Failed to allocate memory"); + closelog(); + return 1; + } + strncpy(opt.peer_cert_path, peer_cert_dir, opt.peer_cert_dir_len); + } + + /* Run the server */ + ret = serve(opt); + + /* Cleanup and exit */ + if (opt.peer_cert_path) + free(opt.peer_cert_path); + closelog(); + return ret; +} diff --git a/tlsd.texi b/tlsd.texi new file mode 100644 index 0000000..8ecb22f --- /dev/null +++ b/tlsd.texi @@ -0,0 +1,577 @@ +\input texinfo +@setfilename tlsd.info +@settitle TLSd + +@direntry +* TLSd: (tlsd). TLS super-server. +@end direntry + +@copying +@quotation +@verbatiminclude COPYING +@end quotation +@end copying + +@include version.texi + +@node Top +@top TLSd +TLSd is a daemon that both accepts and initiates TLS connections, runs +processes, and provides peer certificate's fingerprint as an +environment variable for them. The intent is to facilitate creation +and usage of simple services for peer-to-peer networking. + +This manual is for TLSd version @value{VERSION}, last updated on +@value{UPDATED}. + +@menu +* Copying Conditions:: Your rights. +* Invocation:: Command line arguments. +* Usage:: Basic usage instructions. +* fp2alias:: A basic fingerprint-to-alias converter. +* std2fifo:: A std@{in,out@} <-> <dir>/<env var>/@{in,out@} proxy. +* Common tools:: Using common tools in combination with TLSd. +* Writing services:: Tips and guidelines on how to write services. +@end menu + + +@node Copying Conditions +@chapter TLSd Copying Conditions +@insertcopying + + +@node Invocation +@chapter TLSd invocation + +@example +tlsd [option ...] [--] <command> [argument ...] +@end example + +@section Command line arguments +@table @option +@item -k @var{keyfile} +Private key file to use (default is @file{/etc/tls/key.pem}). + +@item -c @var{certfile} +Certificate file to use (default is @file{/etc/tls/cert.pem}). + +@item -p @var{port} +Port to listen on (default is to use a randomly selected one). + +@item -b @var{host} +Bind address (default is 0.0.0.0). + +@item -s @var{signo} +Send a signal to a child on termination. No signal is sent by default: +child processes are expected to exit once their @code{stdin} is closed. + +@item -n +Do not require a peer certificate. This makes the @env{SHA256} +environment variable for child processes optional. + +@item -d @var{directory} +Write peer certificates in DER format into a directory. + +@item -i @var{ident} +Syslog identifier to use. + +@item -e +Print messages into stderr, in addition to syslog. + +@item -h +Print a help message and exit. +@end table + +@section Examples +@subsection Echo server +@example +tlsd -e cat +@end example + +@subsection Authentication +@example +tlsd -p 5556 -- sh -c 'echo "Hello, $@{SHA256@}! I am a $@{SIDE@}."' +@end example + +@subsection Connection initiation +@example +echo 'localhost 5600' | tlsd -e echo 'hello' +@end example + +@section Signals +The following signals are handled: + +@table @asis +@item @code{SIGINT}, @code{SIGTERM} +Terminate gracefully. +@item @code{SIGHUP} +Reload key and certificate. +@end table + +@node Usage +@chapter TLSd usage + +@section Initiating connections +TLSd reads space-separated hosts and services (ports) from its +@code{stdin}, and initiates connections with those. + +@section Child processes +When TLSd runs child processes, it sets the following environment +variables: + +@table @env +@item SHA256 +Peer's fingerprint: SHA256 hash of their certificate. + +@item SIDE +Either @samp{CLIENT} or @samp{SERVER}, indicates what side of the +connection we are on. +@end table + +A child process can read peer's messages from @code{stdin}, and send +messages to a peer by writing them into @code{stdout}. + + +@node fp2alias +@chapter fp2alias + +fp2alias is a helper program for TLSd. It reads the @env{SHA256} +environment variable, adds the @env{ALIAS} variable by looking it up in +a file, and runs a given command with that variable in the environment. + +If it's not allowed to add new aliases, it would reject unknown users. + +@section Invocation +@example +fp2alias [option ...] [--] [<command> [argument ...]] +@end example + +@subsection Command line arguments +@table @option +@item -f @var{certfile} +A file with "@emph{fingerprint} @emph{alias}" entries (default is +@file{/etc/tls/aliases}). + +@item -a +Add new aliases. This basically turns a private service into a public +one. + +@item -i @var{ident} +Syslog identifier to use. + +@item -e +Print messages into stderr, in addition to syslog. + +@item -h +Print a help message and exit. +@end table + + +@subsection Examples +@subsubsection Authentication +@example +tlsd -- fp2alias -- sh -c 'echo "Hello, $@{ALIAS@}!"' +@end example + + +@node std2fifo +@chapter std2fifo + +std2fifo is a helper program for TLSd. Given a root directory and an +environment variable, it creates a "<root dir>/<env var>/" directory, +writes input into the "out" FIFO in that directory, and prints the "in" +FIFO output. + +Overall, it tries to be suitable for use with TLSd (or other +super-servers), and with common tools on the other end. + +@section Invocation +@example +std2fifo [option ...] [--] <dir> +@end example + +@subsection Command line arguments +@table @option +@item -v @var{var} +An environment variable name (default is @env{SHA256}). + +@item -c +Continuous streams mode: do not reopen streams once they are closed, and +do not close the "out" stream after each message. It is intended for +applications such as file transfer, as opposed to textual messaging. + +@item -i @var{ident} +Syslog identifier to use. + +@item -e +Print messages into stderr, in addition to syslog. + +@item -h +Print a help message and exit. +@end table + +@subsection Examples +@subsubsection Testing +@example +FOO=bar std2fifo -v FOO -ce /tmp/ +@end example + +@subsubsection Per-connection FIFO pairs +@example +tlsd -p 5601 -e -- std2fifo -e /var/lib/tlsd-im/ +@end example + + +@node Common tools +@chapter Common tools +Some of the tools that are handy to use with TLSd are mentioned +here. See their documentation for more information. + +@section Certificate generation +To generate X.509 certificates that are needed for mutual +authentication, one can use GnuTLS: + +@example +certtool --generate-privkey --outfile key.pem +certtool --generate-self-signed --load-privkey key.pem --outfile cert.pem +@end example + +Or OpenSSL: + +@example +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 +@end example + +Add @option{-nodes} to the above command in order to generate an +unencrypted key, for use with @command{tlsd}. + +@section Client connection +To connect to a TLS server, one can also use GnuTLS: + +@example +gnutls-cli --insecure --x509keyfile=key.pem --x509certfile=cert.pem \ +--port=5556 localhost +@end example + +Or OpenSSL: + +@example +openssl s_client -key key.pem -cert cert.pem -connect localhost:5556 +@end example + +Or ncat: + +@example +nc --ssl --ssl-key key.pem --ssl-cert cert.pem localhost 5556 +@end example + +It may be handy to set aliases to those commands in your shell. + +@subsection rlwrap +@command{rlwrap} (readline wrapper) improves text input, and makes the +above clients usable for chat-like applications. + +@section Tor +Tor hidden services are useful not just for privacy, but also to bypass +NATs, and to have the same address anywhere you go. To setup a hidden +service, simply add into @file{/etc/tor/torrc} something like the +following: + +@example +HiddenServiceDir /var/lib/tor/my-service/ +HiddenServicePort 5556 127.0.0.1:5556 +@end example + +Reload Tor, and @file{/var/lib/tor/my-service/hostname} should contain +your new hostname. + +The clients should be able to connect simply by prefixing their commands +with @command{torify}, and using that hostname. + +When running @command{torify tlsd}, @command{torify} may not like +binding it to 0.0.0.0, but it can be allowed in +@file{/etc/tor/torsocks.conf}. Or just bind to 127.0.0.1, if you don't +want direct incoming connections anyway. + +If you wish to remain anonymous, extra care should be taken. This manual +doesn't cover the topic of anonymity. + +@section SSH +SSH port forwarding is handy for NAT traversal as well, if you have a +remote server: just @command{ssh -R 5600:0.0.0.0:5600 example.com} to +forward incoming connections to your machine. + + +@node Writing services +@chapter Writing services +A service program is expected to write output into its @code{stdout}, +read input from @code{stdin}, and exit when its @code{stdin} is closed +(or upon receiving a signal, which should be specified for +@command{tlsd} in that case). + +While TLSd itself doesn't demand much, a service easily usable with +standard tools requires some care to design. It is suggested to make the +services usable without special client software, with basic shell +commands only. Essentially, to follow the Unix philosophy, and e.g. not +to make up a context-free grammar (one that can't be parsed with regular +expressions properly) where a regular grammar or no parsing at all would +suffice. + +Making the services reusable with other similar super-servers (such as +@command{inetd}, @command{nc -le}, or systemd socket activation) and/or +as interactive programs for local use could also be a good idea. + +@menu +* Security:: Security tips. +* Sample chat:: Designing and setting up a chat. +* Sample file server:: Designing a file server. +* Sample P2P IM:: Setting up instant messaging. +@end menu +@node Security +@section Security +TLSd tries to be simple and minimalistic; it doesn't do much to improve +security, but leaves that to a user. A few tips to consider: + +@itemize @bullet +@item +Set users, groups, and file permissions properly. +@item +Use sandboxing, e.g. SELinux's @command{sandbox}: @command{tlsd -- +sandbox -M my-service}. +@item +Isolate worker processes from master process. For instance, only run a +small client via @command{tlsd}, which would connect to a daemon that +provides actual service, and runs as a separate user (which possibly has +privileges to do what @command{tlsd} can't, but can't read your private +keys). +@item +Limit resource usage (with cgroups, @command{ulimit}, etc). +@item +Use virtualization or dedicated machines. +@item +Use safe languages. For the sake of portability and ease of building, +TLSd itself is written in C, but it's quite a risk. +@end itemize + + +@node Sample chat +@section Sample chat +Let's make a multi-user chat. Ncat can broker connections, so we can set +it as a local daemon, and run more @command{nc} instances as +@command{tlsd} services, using @command{fp2alias} to obtain aliases, and +a basic shell script to prepend those aliases to messages. + +Well, here goes the chat service: + +@example +@verbatiminclude examples/chat/tlsd-chat.sh +@end example + +To try it: +@example +nc -vl --broker 127.0.0.1 7000 +tlsd -p 5600 -- fp2alias -a -- examples/chat/tlsd-chat.sh +rlwrap nc --ssl --ssl-key key.pem --ssl-cert cert.pem localhost 5600 +@end example + +It is usually handy to set daemons to be run by your init system; for +systemd, there are example service files in the @file{examples/chat/} +directory. + +@subsection Client scripting +A client can connect with either @command{rlwrap} and some TLS client, +or a custom program. Or an option between those -- a custom shell +script. For instance, to add a bell when one's name is mentioned, they +can use a script that looks like this: + +@example +@verbatiminclude examples/chat/tls-chat.sh +@end example + + +@node Sample file server +@section Sample file server +Let's make a file server now. One can actually use @command{nginx} and +@command{curl} (and minor HTTP abuse) instead, possibly in combination +with @command{scp} or @command{rsync}, but let's do it anyway -- because +we can, and quite easily. + +@subsection Preparation +Assuming that the certificates are already set, let's create a directory +for files, and make it accessible to both tlsd and our regular user: + +@example +$ sudo mkdir -p /srv/tlsd/files/ +$ sudo chown -R tlsd:tlsd /srv/tlsd/ +$ sudo chmod -R g+w,o-r /srv/tlsd/ +$ sudo gpasswd -a $USER tlsd +@end example + +@subsection File serving +To download files with common tools, a bare minimum is a Gopher-like +protocol where users send file selectors, server sends files, and drops +the connection. Something like this should do: + +@example +@verbatiminclude examples/file-server/serve-files.sh +@end example + +Let's try it: + +@example +$ echo foo > /srv/tlsd/files/bar +$ tlsd -ep 5601 -- serve-files.sh &> tlsd-output & +$ echo bar | openssl s_client -key ~/.tls/key.pem -cert ~/.tls/cert.pem \ + -quiet -connect localhost:5601 +@end example + +@subsection File browsing +The serving works fine, but we don't have a way to browse files yet. For +that, we can use @command{ls}, and perhaps not drop connections: make a +browser for textual files and directory listings. Otherwise protocol can +be the same, no need to complicate things: + +@example +@verbatiminclude examples/file-server/browse-files.sh +@end example + +@subsection File upload +Finally, there should be file upload -- but with some authorization. We +can make users to upload files into per-user directories, and the +presence of a directory itself would mean authorization; to identify +users easier (i.e., not by a SHA256 hash), @command{fp2alias} should be +handy. As of the protocol, it may be similar to the other two: a client +sends a selector followed by a file, and then drops a connection. Here +it goes: + +@example +@verbatiminclude examples/file-server/accept-files.sh +@end example + +Let's see how that works: + +@example +$ tlsd -ep 5603 -- fp2alias -- accept-files.sh &> tlsd-output & +[1] 14063 +$ cat <(echo 'my-file') - | openssl s_client -key ~/.tls/key.pem \ + -cert ~/.tls/cert.pem -connect localhost:5603 +# openssl output skipped +hello +# pressing C-d +DONE +$ fg 1 +tlsd -ep 5603 -- fp2alias -- accept-files.sh &>tlsd-output +^C +$ tail /srv/tlsd/files/$USER/my-file +hello +@end example + + +@node Sample P2P IM +@section Sample peer-to-peer instant messaging +Let's make an IM now. + +@subsection Daemon +We can use @command{tlsd} in combination with @command{std2fifo} to get +a nice @command{ii}-like filesystem layout, to begin with. The daemon +itself could look like this: + +@example +tlsd -p 18765 -- std2fifo /var/lib/tlsd-im/ +@end example + +But we also need to restrict connections, allowing just one per +certificate: + +@example +tlsd -p 18765 -- sh -c 'flock -n "/var/lib/tlsd-im/$@{SHA256@}/lock" \ + std2fifo /var/lib/tlsd-im/' +@end example + +But since we'll be running it in background, some kind of a control +channel should be used. And Tor would be useful to bypass NATs, and some +minor checks would be needed to set file permissions, so the final +couple of scripts, @file{tlsd-im-cmd.sh} and @file{tlsd-im.sh}: + +@example +@verbatiminclude examples/p2p-im/tlsd-im-cmd.sh +@end example + +@example +@verbatiminclude examples/p2p-im/tlsd-im.sh +@end example + +@subsection Connecting +Now we can connect to another @command{tlsd} instance (or any other TLS +server) with a command like this: + +@example +echo 'example.com 18765' > /var/lib/tlsd-im/connect +@end example + +But we want automatic connection restoration on disconnect. So let's put +peer addresses into ``address'' files inside of their directories. Then +we can write a basic script, and set a cron job or a systemd timer for +it: + +@example +@verbatiminclude examples/p2p-im/tlsd-im-reconnect.sh +@end example + +It is not reliable (consider simulatneous connection initiation from +both ends: both could fail, but only one should) and can be improved, +but that's just a few lines of a shell script. + +@subsection UI +The layout we've got: + +@example +/var/lib/tlsd-im/ +|-- <sha256 hash>/ +| |-- in +| |-- out +| |-- address +| `-- lock +|-- <sha256 hash>/ +| |-- in +| |-- out +| |-- address +| `-- lock +`-- connect +@end example + +To make it nicer, we can set aliases with @command{ln}, but it's not +that great without specialized UI, and different users like different +UIs. Fortunately, there is libpurple that powers different IM clients +(pidgin and bitlbee among them), so we can write a plugin for it. + +An example plugin can be found in the @file{examples/p2p-im/} directory, +along with a @file{Makefile} and the above shell scripts. Once the +@file{/var/lib/tlsd-im/} directory is specified as the username, the +plugin simply interacts with those FIFOs, and keeps track of newly +created directories using @code{inotify}. IM clients such as pidgin and +bitlbee allow to set local aliases, so we can leave it up to them. + +@subsection Setup + +What's left is to set it up: run @command{tlsd-im.sh} in background, run +@command{tlsd-im-reconnect.sh} automatically from time to +time. Unfortunately, the right ways to run user daemons seem to vary +among GNU/Linux distributions even more than init systems do. Besides, +@command{bitlbee} normally runs under a separate user, while clients +such as Pidgin run under our regular user. + +One way to solve this is to just set it system-wide, adding all the +users that should be able to access it (such as bitlbee and/or our +regular user) into a dedicated group: + +@example +@verbatiminclude examples/p2p-im/approximate-setup.sh +@end example + +Then one should be able to use it with bitlbee, Pidgin, or other +libpurple-based IM clients. Online status and message delivery tracking +are not great, and generally it can be much better even with plain TLS, +but a usable P2P IM is ready. + +@bye |