From a06cc218bfa18943a46e051d5bbf463e1ddc0b6e Mon Sep 17 00:00:00 2001 From: defanor Date: Sat, 29 Apr 2017 04:36:01 +0300 Subject: Initial commit --- AUTHORS | 1 + COPYING | 24 + ChangeLog | 0 Makefile.am | 18 + NEWS | 12 + README | 48 ++ configure.ac | 35 ++ examples/chat/nc-chatroom.service | 10 + examples/chat/tls-chat.sh | 9 + examples/chat/tlsd-chat.sh | 15 + examples/chat/tlsd-chatroom.service | 13 + examples/file-server/accept-files.sh | 12 + examples/file-server/browse-files.sh | 16 + examples/file-server/serve-files.sh | 13 + examples/p2p-im/Makefile | 33 ++ examples/p2p-im/approximate-setup.sh | 15 + examples/p2p-im/libpurple-fifo-plugin.c | 400 +++++++++++++++ examples/p2p-im/tlsd-im-cmd.sh | 13 + examples/p2p-im/tlsd-im-reconnect.service | 13 + examples/p2p-im/tlsd-im-reconnect.sh | 9 + examples/p2p-im/tlsd-im-reconnect.timer | 8 + examples/p2p-im/tlsd-im.service | 12 + examples/p2p-im/tlsd-im.sh | 11 + fp2alias.1 | 48 ++ fp2alias.c | 144 ++++++ std2fifo.1 | 49 ++ std2fifo.c | 222 ++++++++ tlsd.1 | 67 +++ tlsd.c | 817 ++++++++++++++++++++++++++++++ tlsd.texi | 577 +++++++++++++++++++++ 30 files changed, 2664 insertions(+) create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 ChangeLog create mode 100644 Makefile.am create mode 100644 NEWS create mode 100644 README create mode 100644 configure.ac create mode 100644 examples/chat/nc-chatroom.service create mode 100755 examples/chat/tls-chat.sh create mode 100755 examples/chat/tlsd-chat.sh create mode 100644 examples/chat/tlsd-chatroom.service create mode 100755 examples/file-server/accept-files.sh create mode 100755 examples/file-server/browse-files.sh create mode 100755 examples/file-server/serve-files.sh create mode 100644 examples/p2p-im/Makefile create mode 100644 examples/p2p-im/approximate-setup.sh create mode 100644 examples/p2p-im/libpurple-fifo-plugin.c create mode 100644 examples/p2p-im/tlsd-im-cmd.sh create mode 100644 examples/p2p-im/tlsd-im-reconnect.service create mode 100644 examples/p2p-im/tlsd-im-reconnect.sh create mode 100644 examples/p2p-im/tlsd-im-reconnect.timer create mode 100644 examples/p2p-im/tlsd-im.service create mode 100644 examples/p2p-im/tlsd-im.sh create mode 100644 fp2alias.1 create mode 100644 fp2alias.c create mode 100644 std2fifo.1 create mode 100644 std2fifo.c create mode 100644 tlsd.1 create mode 100644 tlsd.c create mode 100644 tlsd.texi diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..0e17051 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +defanor diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/COPYING @@ -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 diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 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 diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..0a1eeb4 --- /dev/null +++ b/NEWS @@ -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. diff --git a/README b/README new file mode 100644 index 0000000..6d86df8 --- /dev/null +++ b/README @@ -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 + +#include "prpl.h" +#include "version.h" +#include "debug.h" +#include "cmds.h" +#include "accountopt.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#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 + +#include +#include +#include +#include +#include +#include + +/* 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 ...] [--] [ [argument ...]]\n", name); + puts("Options:"); + puts(" -f a file with \" \" entries"); + puts(" -a add new aliases"); + puts(" -i 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} <-> //{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} <-> //{in,out} proxy + + This is free and unencumbered software released into the public + domain. +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ...] \n", name); + puts("Options:"); + puts(" -v an environment variable name"); + puts(" -c continuous streams"); + puts(" -i 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; +} diff --git a/tlsd.1 b/tlsd.1 new file mode 100644 index 0000000..0a3f2d7 --- /dev/null +++ b/tlsd.1 @@ -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. diff --git a/tlsd.c b/tlsd.c new file mode 100644 index 0000000..5462741 --- /dev/null +++ b/tlsd.c @@ -0,0 +1,817 @@ +/* + TLSd, a TLS daemon. + + This is free and unencumbered software released into the public + domain. +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 ...] [--] [argument ...]\n", + name); + puts("Options:"); + puts(" -k private key file to use"); + puts(" -c certificate file to use"); + puts(" -p port to listen on"); + puts(" -b bind address"); + puts(" -s a signal to send to a child on termination"); + puts(" -n do not require a peer certificate"); + puts(" -d write peer certificates into a directory"); + puts(" -i 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@} <-> //@{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 ...] [--] [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 ...] [--] [ [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 "//" 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 ...] [--] +@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/ +|-- / +| |-- in +| |-- out +| |-- address +| `-- lock +|-- / +| |-- 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 -- cgit v1.2.3