diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 20 | ||||
-rw-r--r-- | README.7 | 7 | ||||
-rw-r--r-- | calico.1 | 89 | ||||
-rw-r--r-- | dispatch.c | 273 |
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); + } + } +} |