tlsgate

TLS reverse proxy
git clone git://git.akobets.xyz/tlsgate
Log | Files | Refs | README | LICENSE

commit 6f82d1c44496282b335ee1599f7793c6630f8197
Author: Artem Kobets <artem@akobets.xyz>
Date:   Sat,  1 Aug 2020 17:17:42 +0300

initial commit

Diffstat:
A.gitignore | 2++
ALICENSE | 15+++++++++++++++
AMakefile | 33+++++++++++++++++++++++++++++++++
AREADME | 12++++++++++++
Aarg.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 16++++++++++++++++
Amain.c | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve.c | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aserve.h | 1+
Asock.c | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asock.h | 3+++
Atlsgate.1 | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.c | 27+++++++++++++++++++++++++++
Autil.h | 2++
14 files changed, 615 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +*.o +tlsgate diff --git a/LICENSE b/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2020 Artem Kobets <artem@akobets.xyz> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,33 @@ +.POSIX: + +include config.mk + +SRC = main.c serve.c sock.c util.c +OBJ = $(SRC:.c=.o) + +all: tlsgate + +.c.o: + $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $@ +$(OBJ): config.mk +util.o: util.h +sock.o: sock.h util.h +main.o: arg.h serve.h sock.h util.h +serve.o: serve.h util.h + +tlsgate: $(OBJ) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJ) + +clean: + rm -f tlsgate $(OBJ) + +install: tlsgate + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f tlsgate $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/tlsgate + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + cp -f tlsgate.1 $(DESTDIR)$(MANPREFIX)/man1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/tlsgate + rm -f $(DESTDIR)$(MANPREFIX)/man1/tlsgate.1 diff --git a/README b/README @@ -0,0 +1,12 @@ +tlsgate +------- +A simple TLS proxy for unencrypted connections. +Can be used to set up an HTTPS connection for an HTTP server. + +Dependencies +------------ +- libtls, a TLS library provided by LibreSSL. + +Install +------- +make install diff --git a/arg.h b/arg.h @@ -0,0 +1,49 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][1]\ + && argv[0][0] == '-';\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.mk b/config.mk @@ -0,0 +1,16 @@ +VERSION = 0.0.1 + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +# includes and libs +LIBS = -ltls + +# flags +CPPFLAGS = -DVERSION=\"$(VERSION)\" -D_POSIX_C_SOURCE=200809L +CFLAGS = -std=c99 -pedantic -Wall -Os +LDFLAGS = $(LIBS) -s + +# compiler and linker +CC = cc diff --git a/main.c b/main.c @@ -0,0 +1,191 @@ +#include <errno.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/resource.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <tls.h> + +#include "arg.h" +#include "serve.h" +#include "sock.h" +#include "util.h" + +char *argv0; + +void sigchld(int unused); +void usage(); + +void +sigchld(int unused) +{ + while (waitpid(-1, NULL, WNOHANG) > 0); +} + +void +usage() +{ + die( + "usage: %s -c cert -k key [-a ca]\n" + " [-h host] -p port [-H host] [-P port] [-U file]\n" + " [-n proc-num]\n", + argv0 + ); +} + +int +main(int argc, char **argv) +{ + char *cert_file = NULL; + char *key_file = NULL; + char *ca_file = NULL; + char *server_host = NULL; + char *server_port = NULL; + char *client_host = NULL; + char *client_port = NULL; + char *client_udsfile = NULL; + int maxnprocs = 512; + + struct rlimit rlim; + struct tls *ctx; + struct tls_config *config; + int fd; + struct sigaction act; + sigset_t sigmask; + + ARGBEGIN { + case 'c': + cert_file = EARGF(usage()); + break; + case 'k': + key_file = EARGF(usage()); + break; + case 'a': + ca_file = EARGF(usage()); + break; + case 'n': + maxnprocs = atol(EARGF(usage())); + break; + case 'h': + server_host = EARGF(usage()); + break; + case 'p': + server_port = EARGF(usage()); + break; + case 'H': + client_host = EARGF(usage()); + break; + case 'P': + client_port = EARGF(usage()); + break; + case 'U': + client_udsfile = EARGF(usage()); + break; + default: + usage(); + break; + } ARGEND + + /* cert and private key files are required */ + if (cert_file == NULL || key_file == NULL) + usage(); + /* server host can be NULL, port is required */ + if (server_port == NULL) + usage(); + /* allow IPS or UDS client */ + if ( + (client_host != NULL && client_udsfile != NULL) || + !(client_port != NULL || client_udsfile != NULL) + ) + usage(); + + /* process limit */ + rlim.rlim_cur = rlim.rlim_max = maxnprocs; + if (setrlimit(RLIMIT_NPROC, &rlim) < 0) + die("setrlimit RLIMIT_NPROC: %s\n", strerror(errno)); + + /* setup tls */ + if ((ctx = tls_server()) == NULL) + die("tls_server: %s\n", strerror(errno)); + if ((config = tls_config_new()) == NULL) + die("tls_config_new: %s\n", strerror(errno)); + if (tls_config_set_cert_file(config, cert_file) < 0) + die("tls_config_set_cert_file: %s\n", strerror(errno)); + if (tls_config_set_key_file(config, key_file) < 0) + die("tls_config_set_key_file: %s\n", strerror(errno)); + if (ca_file != NULL) { + if (tls_config_set_ca_file(config, ca_file) < 0) + die("tls_config_set_ca_file: %s\n", strerror(errno)); + } + if (tls_configure(ctx, config) < 0) + die("tls_configure: %s\n", strerror(errno)); + + /* setup server socket */ + fd = sock_server_ips(server_host, server_port); + + /* reap children */ + act.sa_handler = sigchld; + sigemptyset(&sigmask); + act.sa_mask = sigmask; + act.sa_flags = 0; + sigaction(SIGCHLD, &act, NULL); + + while (1) { + int pid; + int cfd = -1, clientfd = -1; + struct tls *cctx = NULL; + + if ((cfd = accept(fd, NULL, NULL)) < 0) { + /* can be interrupted with SIGCHLD */ + if (errno != EINTR) + warn("accept: %s\n", strerror(errno)); + continue; + } + + switch (pid = fork()) { + case 0: + close(fd); + + /* start tls */ + if (tls_accept_socket(ctx, &cctx, cfd) < 0) { + warn("tls_accept_socket: %s\n", strerror(errno)); + goto cleanup; + } + if (tls_handshake(cctx) < 0) { + warn("tls_handshake: %s\n", tls_error(cctx)); + goto cleanup; + } + + /* connect to client */ + clientfd = client_udsfile + ? sock_client_uds(client_udsfile) + : sock_client_ips(client_host, client_port); + if (clientfd < 0) + goto cleanup; + + serve(cctx, clientfd); + +cleanup: + if (cctx != NULL) { + tls_close(cctx); + tls_free(cctx); + } + shutdown(cfd, SHUT_RDWR); + close(cfd); + if (clientfd >= 0) + close(clientfd); + _exit(EXIT_SUCCESS); + case -1: + warn("fork: %s\n", strerror(errno)); + /* fallthrough */ + default: + /* close connection in parent */ + close(cfd); + break; + } + } +} diff --git a/serve.c b/serve.c @@ -0,0 +1,95 @@ +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/wait.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <tls.h> + +#include "serve.h" +#include "util.h" + +#define BUFMAX 4096 + +void +serve( + struct tls *ctx, + int fd +) +{ + int pid; + char buf[BUFMAX]; + + pid = fork(); + if (pid < 0) { + warn("fork: %s\n", strerror(errno)); + tls_close(ctx); + shutdown(fd, SHUT_RDWR); + return; + } + /* proxy - parent and child connect + * encrypted socket and unencrypted socket + * until one of the connections is terminated */ + if (pid == 0) { + while (1) { + ssize_t nread, nwrite; + + nread = read(fd, buf, sizeof(buf)); + if (nread < 0) { + break; + } + if (nread == 0) { + break; + } + + while (1) { + nwrite = tls_write(ctx, buf, nread); + if ( + nwrite == TLS_WANT_POLLIN || + nwrite == TLS_WANT_POLLOUT + ) { + continue; + } else { + break; + } + } + if (nwrite < 0) { + break; + } + } + tls_close(ctx); + shutdown(fd, SHUT_RDWR); + _exit(EXIT_SUCCESS); + } else { + while (1) { + ssize_t nread, nwrite; + + while (1) { + nread = tls_read(ctx, buf, sizeof(buf)); + if ( + nread == TLS_WANT_POLLIN || + nread == TLS_WANT_POLLOUT + ) { + continue; + } else { + break; + } + } + if (nread < 0) { + break; + } + if (nread == 0) { + break; + } + + nwrite = write(fd, buf, nread); + if (nwrite < 0) { + break; + } + } + tls_close(ctx); + shutdown(fd, SHUT_RDWR); + } +} diff --git a/serve.h b/serve.h @@ -0,0 +1 @@ +void serve(struct tls *cctx, int clientfd); diff --git a/sock.c b/sock.c @@ -0,0 +1,116 @@ +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <arpa/inet.h> +#include <netinet/in.h> +#include <netdb.h> +#include <sys/un.h> + +#include "util.h" + +int +sock_server_ips(const char *host, const char *port) +{ + struct addrinfo hints = { + .ai_flags = AI_NUMERICSERV, + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + }; + int ret; + struct addrinfo *ai, *p; + int fd = -1; + int optval = 1; + + if ((ret = getaddrinfo(host, port, &hints, &ai)) != 0) + die("getaddrinfo: %s\n", gai_strerror(ret)); + + for (p = ai; p != NULL; p = p->ai_next) { + fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (fd < 0) + continue; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) + die("setsockopt: %s\n", strerror(errno)); + if (bind(fd, p->ai_addr, p->ai_addrlen) < 0) { + close(fd); + } else { + break; + } + } + freeaddrinfo(ai); + if (p == NULL) + die( + "bind: Can not bind to address: %s:%s\n", + host, + port + ); + + if (listen(fd, SOMAXCONN) < 0) + die("listen: %s\n", strerror(errno)); + + return fd; +} + +int +sock_client_ips(const char *host, const char *port) +{ + struct addrinfo hints = { + .ai_flags = AI_NUMERICSERV, + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + }; + int ret; + struct addrinfo *ai, *p; + int fd = -1; + + if ((ret = getaddrinfo(host, port, &hints, &ai)) != 0) { + warn("getaddrinfo: %s\n", gai_strerror(ret)); + return -1; + } + + for (p = ai; p != NULL; p = p->ai_next) { + fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (fd < 0) + continue; + if (connect(fd, p->ai_addr, p->ai_addrlen) < 0) { + close(fd); + } else { + break; + } + } + freeaddrinfo(ai); + if (p == NULL) { + warn( + "connect: Can not connect to address: %s:%s\n", + host, + port + ); + return -1; + } + + return fd; +} + +int +sock_client_uds(const char *file) +{ + int fd; + struct sockaddr_un addr; + + if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) { + warn("socket: %s\n", strerror(errno)); + return -1; + } + + addr.sun_family = AF_UNIX; + snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", file); + + if (connect(fd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)) == -1) { + warn("connect: %s\n", strerror(errno)); + return -1; + } + + return fd; +} diff --git a/sock.h b/sock.h @@ -0,0 +1,3 @@ +int sock_server_ips(const char *host, const char *port); +int sock_client_ips(const char *host, const char *port); +int sock_client_uds(const char *file); diff --git a/tlsgate.1 b/tlsgate.1 @@ -0,0 +1,53 @@ +.TH TLSGATE 1 2020-08-01 +.SH NAME +tlsgate \- tls proxy +.SH SYNOPSIS +.B tlsgate -c cert -k key [-a ca] [-h host] +-p port [-H host] [-P port] [-U file] +[-n proc-num] +.SH DESCRIPTION +.B tlsgate +is a simple tls proxy which can be used to expose an unencrypted connection. +For example, to set up an HTTPS connection for an HTTP server. +.SH OPTIONS +.TP +.B \-c cert +Path to certificate. +.TP +.B \-k key +Path to private key. +.TP +.B \-a ca +Path to CA root certificates. +.TP +.B \-h host +TLS server's hostname. +.TP +.B \-p port +TLS server's port number. +.TP +.B \-H host +Client's hostname. +.TP +.B \-P port +Client's port number. +.TP +.B \-U file +Client's UNIX domain socket path. +.TP +.B \-n proc-num +Maximum number of threads. Default is 512. +.SH EXAMPLES +# accept connections on port 443 +.br +# and pass them to a local http server on port 80 +.br +tlsgate \\ + -c /etc/path-to-cert/cert.pem \\ + -k /etc/path-to-key/key.pem \\ + -h 0.0.0.0 \\ + -p 443 \\ + -H 0.0.0.0 \\ + -P 80 +.SH AUTHOR +Artem Kobets <artem@akobets.xyz> diff --git a/util.c b/util.c @@ -0,0 +1,27 @@ +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + +void +warn(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + exit(EXIT_FAILURE); +} diff --git a/util.h b/util.h @@ -0,0 +1,2 @@ +void warn(const char *fmt, ...); +void die(const char *fmt, ...);