summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Makefile20
-rw-r--r--README.77
-rw-r--r--calico.189
-rw-r--r--dispatch.c273
5 files changed, 382 insertions, 8 deletions
diff --git a/.gitignore b/.gitignore
index ab1ddcf..c66cc95 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 *.conf
 *.o
+calico
 config.mk
 localhost.crt
 localhost.key
diff --git a/Makefile b/Makefile
index c0b8323..62cf6b0 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,9 @@ LDLIBS = -ltls
 
 -include config.mk
 
+BINS = calico pounce
+MANS = ${BINS:=.1}
+
 OBJS += bounce.o
 OBJS += client.o
 OBJS += config.o
@@ -18,7 +21,10 @@ OBJS += ring.o
 OBJS += server.o
 OBJS += state.o
 
-all: tags pounce
+all: tags ${BINS}
+
+calico: dispatch.o
+	${CC} ${LDFLAGS} dispatch.o -o $@
 
 pounce: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
@@ -29,16 +35,18 @@ tags: *.c *.h
 	ctags -w *.c *.h
 
 clean:
-	rm -f tags pounce ${OBJS}
+	rm -f tags ${BINS} ${OBJS} dispatch.o
 
-install: pounce pounce.1 rc.pounce
+install: ${BINS} ${MANS} rc.pounce
 	install -d ${PREFIX}/bin ${MANDIR}/man1 ${ETCDIR}/rc.d
-	install pounce ${PREFIX}/bin
-	install -m 644 pounce.1 ${MANDIR}/man1
+	install ${BINS} ${PREFIX}/bin
+	install -m 644 ${MANS} ${MANDIR}/man1
 	install rc.pounce ${ETCDIR}/rc.d/pounce
 
 uninstall:
-	rm -f ${PREFIX}/bin/pounce ${MANDIR}/man1/pounce.1 ${ETCDIR}/rc.d/pounce
+	rm -f ${BINS:%=${PREFIX}/bin/%}
+	rm -f ${MANS:%=${MANDIR}/man1/%}
+	rm -f ${ETCDIR}/rc.d/pounce
 
 localhost.crt:
 	printf "[dn]\nCN=localhost\n[req]\ndistinguished_name=dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth" \
diff --git a/README.7 b/README.7
index c1a6806..4225afa 100644
--- a/README.7
+++ b/README.7
@@ -1,4 +1,4 @@
-.Dd October 29, 2019
+.Dd November 1, 2019
 .Dt README 7
 .Os "Causal Agency"
 .
@@ -48,7 +48,7 @@ all events can be accurately replayed,
 rather than being limited to messages.
 .
 .Sh FILES
-.Bl -tag -width "rc.pounce" -compact
+.Bl -tag -width "dispatch.c" -compact
 .It Pa bounce.h
 common declarations and default paths
 .It Pa bounce.c
@@ -66,6 +66,8 @@ buffer between server and clients
 .It Pa config.c
 .Xr getopt_long 3 Ns -integrated
 configuration parsing
+.It Pa dispatch.c
+SNI socket dispatcher
 .It Pa rc.pounce
 .Fx
 .Xr rc 8
@@ -73,4 +75,5 @@ script
 .El
 .
 .Sh SEE ALSO
+.Xr calico 1 ,
 .Xr pounce 1
diff --git a/calico.1 b/calico.1
new file mode 100644
index 0000000..d1475ab
--- /dev/null
+++ b/calico.1
@@ -0,0 +1,89 @@
+.Dd November 1, 2019
+.Dt CALICO 1
+.Os
+.
+.Sh NAME
+.Nm calico
+.Nd dispatches cat
+.
+.Sh SYNOPSIS
+.Nm
+.Op Fl H Ar host
+.Op Fl P Ar port
+.Op Fl t Ar timeout
+.Ar directory
+.
+.Sh DESCRIPTION
+The
+.Nm
+daemon
+dispatches incoming TLS connections
+to instances of
+.Xr pounce 1
+by Server Name Identification (SNI).
+\"(TODO: Explain how to configure pounce for this.
+.
+.Pp
+The arguments are as follows:
+.Bl -tag -width Ds
+.It Fl H Ar host
+Bind to
+.Ar host .
+The default host is localhost.
+.It Fl P Ar port
+Bind to
+.Ar port .
+The default port is 6697.
+.It Fl t Ar timeout
+Set the timeout in milliseconds
+after which a connection will be closed
+if it has not sent the ClientHello message.
+The default timeout is 1000 milliseconds.
+.It Ar directory
+The path to the directory containing
+.Xr pounce 1
+UNIX-domain sockets.
+.El
+.
+.Sh EXAMPLES
+\"(TODO: An example with two pounce instances and a calico.
+.
+.Sh STANDARDS
+The
+.Nm
+daemon implements the following:
+.
+.Bl -item
+.It
+.Rs
+.%A E. Rescorla
+.%Q Mozilla
+.%T The Transport Layer Security (TLS) Protocol Version 1.3
+.%I IETF
+.%N RFC 8446
+.%D August 2018
+.%U https://tools.ietf.org/html/rfc8446
+.Re
+.
+.It
+.Rs
+.%A D. Eastlake 3rd
+.%Q Huawei
+.%T Transport Layer Security (TLS) Extensions: Extension Definitions
+.%I IETF
+.%N RFC 6066
+.%D January 2011
+.%U https://tools.ietf.org/html/rfc6066
+.Re
+.El
+.
+.Sh AUTHORS
+.An June Bug Aq Mt june@causal.agency
+.
+.Sh BUGS
+Send mail to
+.Aq Mt june@causal.agency
+or join
+.Li #ascii.town
+on
+.Li chat.freenode.net .
diff --git a/dispatch.c b/dispatch.c
new file mode 100644
index 0000000..3f28e7f
--- /dev/null
+++ b/dispatch.c
@@ -0,0 +1,273 @@
+/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <poll.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+static struct {
+	struct pollfd *ptr;
+	size_t len, cap;
+} event;
+
+static void eventAdd(int fd) {
+	if (event.len == event.cap) {
+		event.cap = (event.cap ? event.cap * 2 : 8);
+		event.ptr = realloc(event.ptr, sizeof(*event.ptr) * event.cap);
+		if (!event.ptr) err(EX_OSERR, "malloc");
+	}
+	event.ptr[event.len++] = (struct pollfd) {
+		.fd = fd,
+		.events = POLLIN,
+	};
+}
+
+static void eventRemove(size_t i) {
+	close(event.ptr[i].fd);
+	event.ptr[i] = event.ptr[--event.len];
+}
+
+static ssize_t sendfd(int sock, int fd) {
+	size_t len = CMSG_SPACE(sizeof(int));
+	char buf[len];
+
+	char x = 0;
+	struct iovec iov = { .iov_base = &x, .iov_len = 1 };
+	struct msghdr msg = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+		.msg_control = buf,
+		.msg_controllen = len,
+	};
+
+	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+	cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	*(int *)CMSG_DATA(cmsg) = fd;
+
+	return sendmsg(sock, &msg, 0);
+}
+
+static struct {
+	uint8_t buf[4096];
+	uint8_t *ptr;
+	size_t len;
+} peek;
+
+static void skip(size_t skip) {
+	if (peek.len < skip) skip = peek.len;
+	peek.ptr += skip;
+	peek.len -= skip;
+}
+static uint8_t uint8(void) {
+	if (peek.len < 1) return 0;
+	peek.len--;
+	return *peek.ptr++;
+}
+static uint16_t uint16(void) {
+	uint16_t val = uint8();
+	return val << 8 | uint8();
+}
+
+static char *serverName(void) {
+	peek.ptr = peek.buf;
+	// TLSPlaintext
+	if (uint8() != 22) return NULL;
+	skip(4);
+	// Handshake
+	if (uint8() != 1) return NULL;
+	skip(3);
+	// ClientHello
+	skip(34);
+	skip(uint8());
+	skip(uint16());
+	skip(uint8());
+	peek.len = uint16();
+	while (peek.len) {
+		// Extension
+		uint16_t type = uint16();
+		uint16_t len = uint16();
+		if (type != 0) {
+			skip(len);
+			continue;
+		}
+		// ServerNameList
+		skip(2);
+		// ServerName
+		if (uint8() != 0) return NULL;
+		// HostName
+		len = uint16();
+		char *name = (char *)peek.ptr;
+		skip(len);
+		*peek.ptr = '\0';
+		return name;
+	}
+	return NULL;
+}
+
+int main(int argc, char *argv[]) {
+	const char *host = "localhost";
+	const char *port = "6697";
+	const char *path = NULL;
+	int timeout = 1000;
+
+	int opt;
+	while (0 < (opt = getopt(argc, argv, "H:P:t:"))) {
+		switch (opt) {
+			break; case 'H': host = optarg;
+			break; case 'P': port = optarg;
+			break; case 't': {
+				char *rest;
+				timeout = strtol(optarg, &rest, 0);
+				if (*rest) errx(EX_USAGE, "invalid timeout: %s", optarg);
+			}
+			break; default:  return EX_USAGE;
+		}
+	}
+	if (optind < argc) {
+		path = argv[optind];
+	} else {
+		errx(EX_USAGE, "directory required");
+	}
+
+	int dir = open(path, O_DIRECTORY);
+	if (dir < 0) err(EX_NOINPUT, "%s", path);
+
+	int error = fchdir(dir);
+	if (error) err(EX_NOINPUT, "%s", path);
+
+	struct addrinfo *head;
+	struct addrinfo hints = {
+		.ai_family = AF_UNSPEC,
+		.ai_socktype = SOCK_STREAM,
+		.ai_protocol = IPPROTO_TCP,
+	};
+	error = getaddrinfo(host, port, &hints, &head);
+	if (error) errx(EX_NOHOST, "%s:%s: %s", host, port, gai_strerror(error));
+
+	size_t binds = 0;
+	for (struct addrinfo *ai = head; ai; ai = ai->ai_next) {
+		int sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
+		if (sock < 0) err(EX_OSERR, "socket");
+
+		int yes = 1;
+		error = setsockopt(
+			sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)
+		);
+		if (error) err(EX_OSERR, "setsockopt");
+
+		error = bind(sock, ai->ai_addr, ai->ai_addrlen);
+		if (error) {
+			warn("%s:%s", host, port);
+			close(sock);
+			continue;
+		}
+
+		eventAdd(sock);
+		binds++;
+	}
+	if (!binds) errx(EX_UNAVAILABLE, "could not bind any sockets");
+	freeaddrinfo(head);
+
+	for (size_t i = 0; i < binds; ++i) {
+		error = listen(event.ptr[i].fd, 1);
+		if (error) err(EX_IOERR, "listen");
+	}
+
+	for (;;) {
+		int nfds = poll(
+			event.ptr, event.len, (event.len > binds ? timeout : -1)
+		);
+		if (nfds < 0) err(EX_IOERR, "poll");
+
+		if (!nfds) {
+			for (size_t i = event.len - 1; i >= binds; --i) {
+				eventRemove(i);
+			}
+			continue;
+		}
+
+		for (size_t i = event.len - 1; i < event.len; --i) {
+			if (!event.ptr[i].revents) continue;
+
+			if (i < binds) {
+				int sock = accept(event.ptr[i].fd, NULL, NULL);
+				if (sock < 0) err(EX_IOERR, "accept");
+
+				int yes = 1;
+				error = setsockopt(
+					sock, SOL_SOCKET, SO_NOSIGPIPE, &yes, sizeof(yes)
+				);
+				if (error) err(EX_OSERR, "setsockopt");
+
+				eventAdd(sock);
+				continue;
+			}
+
+			if (event.ptr[i].revents & (POLLHUP | POLLERR)) {
+				eventRemove(i);
+				continue;
+			}
+
+			ssize_t len = recv(
+				event.ptr[i].fd, peek.buf, sizeof(peek.buf) - 1, MSG_PEEK
+			);
+			if (len < 0) {
+				warn("recv");
+				eventRemove(i);
+				continue;
+			}
+			peek.len = len;
+
+			char *name = serverName();
+			if (!name || name[0] == '.' || name[0] == '/') {
+				eventRemove(i);
+				continue;
+			}
+
+			int sock = socket(PF_UNIX, SOCK_STREAM, 0);
+			if (sock < 0) err(EX_OSERR, "socket");
+
+			struct sockaddr_un addr = { .sun_family = AF_UNIX };
+			strncpy(addr.sun_path, name, sizeof(addr.sun_path));
+#ifdef __FreeBSD__
+			error = connectat(
+				dir, sock, (struct sockaddr *)&addr, SUN_LEN(&addr)
+			);
+#else
+			error = connect(sock, (struct sockaddr *)&addr, SUN_LEN(&addr));
+#endif
+			if (error) warn("%s", name);
+
+			len = sendfd(sock, event.ptr[i].fd);
+			if (len < 0) warn("%s", name);
+
+			close(sock);
+			eventRemove(i);
+		}
+	}
+}