summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordefanor <defanor@uberspace.net>2017-04-29 04:36:01 +0300
committerdefanor <defanor@uberspace.net>2017-04-29 04:36:01 +0300
commita06cc218bfa18943a46e051d5bbf463e1ddc0b6e (patch)
treed160d59dc14e0693ac8a304574dbf726b92850a0
Initial commit
-rw-r--r--AUTHORS1
-rw-r--r--COPYING24
-rw-r--r--ChangeLog0
-rw-r--r--Makefile.am18
-rw-r--r--NEWS12
-rw-r--r--README48
-rw-r--r--configure.ac35
-rw-r--r--examples/chat/nc-chatroom.service10
-rwxr-xr-xexamples/chat/tls-chat.sh9
-rwxr-xr-xexamples/chat/tlsd-chat.sh15
-rw-r--r--examples/chat/tlsd-chatroom.service13
-rwxr-xr-xexamples/file-server/accept-files.sh12
-rwxr-xr-xexamples/file-server/browse-files.sh16
-rwxr-xr-xexamples/file-server/serve-files.sh13
-rw-r--r--examples/p2p-im/Makefile33
-rw-r--r--examples/p2p-im/approximate-setup.sh15
-rw-r--r--examples/p2p-im/libpurple-fifo-plugin.c400
-rw-r--r--examples/p2p-im/tlsd-im-cmd.sh13
-rw-r--r--examples/p2p-im/tlsd-im-reconnect.service13
-rw-r--r--examples/p2p-im/tlsd-im-reconnect.sh9
-rw-r--r--examples/p2p-im/tlsd-im-reconnect.timer8
-rw-r--r--examples/p2p-im/tlsd-im.service12
-rw-r--r--examples/p2p-im/tlsd-im.sh11
-rw-r--r--fp2alias.148
-rw-r--r--fp2alias.c144
-rw-r--r--std2fifo.149
-rw-r--r--std2fifo.c222
-rw-r--r--tlsd.167
-rw-r--r--tlsd.c817
-rw-r--r--tlsd.texi577
30 files changed, 2664 insertions, 0 deletions
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..0e17051
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+defanor <defanor@uberspace.net>
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 <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
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 <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;
+}
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 <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