commit - 9f898869e18e5d2bda5be22b76ea0f3e7a5b1a17
commit + d188f690caee2963ffc764d63be8e982efc6f8cf
blob - cfdb33ddbb45d3338d840aa96edd139cd764a073
blob + c5b762ec32da795089cdbdd036656e61b802c122
--- .gitignore
+++ .gitignore
kamictl/kamictl
kamid/kamid
kamiftp/kamiftp
+kamiproxy/kamiproxy
kamirepl/kamirepl
ninepscript/ninepscript
blob - 7371569608ee7956f2692a05bd7d4bea1b0b1da4
blob + 56ae9bef5c418e159f8c26553eb183ed814a4932
--- Makefile.am
+++ Makefile.am
-SUBDIRS = compat contrib kamictl kamid kamiftp kamirepl ninepscript regress
+SUBDIRS = compat \
+ contrib \
+ kamictl \
+ kamid \
+ kamiftp \
+ kamiproxy \
+ kamirepl \
+ ninepscript \
+ regress
AM_CPPFLAGS += -DKAMID_VERSION='"@VERSION"' \
-I$(top_srcdir)/lib \
blob - 48edbe53802a562bf7717bd147433e8f3991257e
blob + b4ac79e0f2e9e524bec210c244d593a3ab4888ef
--- configure.ac
+++ configure.ac
kamictl/Makefile
kamid/Makefile
kamiftp/Makefile
+ kamiproxy/Makefile
kamirepl/Makefile
ninepscript/Makefile
regress/Makefile
blob - /dev/null
blob + 42c3cfb97e571db92c7c6853aadb9bd63379e54f (mode 644)
--- /dev/null
+++ kamiproxy/Makefile.am
+bin_PROGRAMS = kamiproxy
+
+kamiproxy_SOURCES = proxy.c \
+ $(top_srcdir)/lib/log.c \
+ $(top_srcdir)/lib/log.h
+
+dist_man1_MANS = kamiproxy.1
+
+kamiproxy_LDADD = $(top_builddir)/compat/libopenbsd-compat.a
+kamiproxy_DEPENDENCIES = $(top_builddir)/compat/libopenbsd-compat.a
+
+AM_CPPFLAGS += -DKAMID_VERSION='"@VERSION@"' \
+ -I$(top_srcdir)/ \
+ -I$(top_srcdir)/compat \
+ -I$(top_srcdir)/lib
blob - /dev/null
blob + c6b9d1c2e4f93d7095bd990dcbbea33a1fe4265d (mode 644)
--- /dev/null
+++ kamiproxy/kamiproxy.1
+.\" Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+.\"
+.\" Permission to use, copy, modify, and 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.
+.\"
+.Dd February 5, 2022
+.Dt KAMIPROXY 1
+.Os
+.Sh NAME
+.Nm kamiproxy
+.Nd 9p over tls proxy
+.Sh SYNOPSIS
+.Nm
+.Op Fl dv
+.Fl c Ar host Ns Oo : Ns Ar port Oc
+.Fl l Oo Ar host : Oc Ns port
+.Fl C Ar cert
+.Op Fl K Ar key
+.Sh DESCRIPTION
+.Nm
+is a proxy for 9p over tls.
+It listen on a local port for plaintext 9p connections and forwards
+them to a real server using a TLS-encrypted tunnel and a client
+certificate.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl C Ar cert
+Path to the client certificate to use.
+.It Fl c Ar host Ns Oo : Ns Ar port Oc
+Connect to the remote server identified by the given
+.Ar host
+name on the specified
+.Ar port
+.Pq 1337 by default.
+.It Fl d
+Do not daemonize.
+If this option is specified,
+.Nm
+will run in the foreground and log to standard error.
+.It Fl K Ar key
+Path to the key of the client certificate.
+If not provided, it's assumed to be the same as the
+.Fl C
+flag.
+.It Fl l Oo Ar host : Oc Ns port
+Listen on the specified address.
+The
+.Ar host
+by default is
+.Dq localhost .
+.It Fl v
+Produce more verbose output.
+.El
+.Sh SEE ALSO
+.Xr 9p 7 ,
+.Xr kamid 8
+.Sh AUTHORS
+.An -nosplit
+The
+.Nm
+program was written by
+.An Omar Polo Aq Mt op@omarpolo.com .
+.Sh BUGS
+.Nm
+opens one TLS-encrypted connection for each incoming connection.
+A better approach would be to multiplex the traffic to the remote
+server, akin to what the plan9 kernel does by default.
blob - /dev/null
blob + 533f84b4cc24c8a43ab20c1a50f9b6c30a4b4fb0 (mode 644)
--- /dev/null
+++ kamiproxy/proxy.c
+/*
+ * Copyright (c) 2022 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and 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.
+ */
+
+#include "compat.h"
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <netdb.h>
+
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <tls.h>
+#include <unistd.h>
+
+#include "log.h"
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+int debug;
+int verbose;
+const char *tohost;
+const char *fromhost;
+
+uint8_t *cert;
+size_t certlen;
+uint8_t *key;
+size_t keylen;
+
+#define MAXSOCK 32
+struct event sockev[MAXSOCK];
+int socks[MAXSOCK];
+int nsock;
+
+char kamihost[64];
+char kamiport[8];
+
+struct conn {
+ struct tls *ctx;
+ struct bufferevent *server;
+ int kfd;
+ struct bufferevent *client;
+ int lfd;
+};
+
+#ifndef __OpenBSD__
+# define pledge(a, b) (0)
+#endif
+
+static const char *
+copysec(const char *s, char *d, size_t len)
+{
+ const char *c;
+
+ if ((c = strchr(s, ':')) == NULL)
+ return NULL;
+ if ((size_t)(c-s) >= len-1)
+ return NULL;
+ memset(d, 0, len);
+ memcpy(d, s, c - s);
+ return c;
+}
+
+static void
+parse_tohost(void)
+{
+ const char *c;
+
+ if ((c = strchr(tohost, ':')) == NULL) {
+ strlcpy(kamihost, tohost, sizeof(kamihost));
+ strlcpy(kamiport, "1337", sizeof(kamiport));
+ return;
+ }
+
+ if ((c = copysec(tohost, kamihost, sizeof(kamihost))) == NULL)
+ fatalx("hostname too long: %s", tohost);
+
+ strlcpy(kamiport, c+1, sizeof(kamiport));
+}
+
+static void
+tls_readcb(int fd, short event, void *d)
+{
+ struct bufferevent *bufev = d;
+ struct conn *conn = bufev->cbarg;
+ char buf[IBUF_READ_SIZE];
+ int what = EVBUFFER_READ;
+ int howmuch = IBUF_READ_SIZE;
+ ssize_t ret;
+ size_t len;
+
+ if (event == EV_TIMEOUT) {
+ what |= EVBUFFER_TIMEOUT;
+ goto err;
+ }
+
+ if (bufev->wm_read.high != 0)
+ howmuch = MIN(sizeof(buf), bufev->wm_read.high);
+
+ switch (ret = tls_read(conn->ctx, buf, howmuch)) {
+ case TLS_WANT_POLLIN:
+ case TLS_WANT_POLLOUT:
+ goto retry;
+ case -1:
+ what |= EVBUFFER_ERROR;
+ goto err;
+ }
+ len = ret;
+
+ if (len == 0) {
+ what |= EVBUFFER_EOF;
+ goto err;
+ }
+
+ if (evbuffer_add(bufev->input, buf, len) == -1) {
+ what |= EVBUFFER_ERROR;
+ goto err;
+ }
+
+ event_add(&bufev->ev_read, NULL);
+
+ len = EVBUFFER_LENGTH(bufev->input);
+ if (bufev->wm_read.low != 0 && len < bufev->wm_read.low)
+ return;
+ if (bufev->wm_read.high != 0 && len > bufev->wm_read.high) {
+ /*
+ * here we could implement some read pressure
+ * mechanism.
+ */
+ }
+
+ if (bufev->readcb != NULL)
+ (*bufev->readcb)(bufev, bufev->cbarg);
+
+ return;
+
+retry:
+ event_add(&bufev->ev_read, NULL);
+ return;
+
+err:
+ (*bufev->errorcb)(bufev, what, bufev->cbarg);
+}
+
+static void
+tls_writecb(int fd, short event, void *d)
+{
+ struct bufferevent *bufev = d;
+ struct conn *conn = bufev->cbarg;
+ ssize_t ret;
+ size_t len;
+ short what = EVBUFFER_WRITE;
+
+ if (event == EV_TIMEOUT) {
+ what |= EVBUFFER_TIMEOUT;
+ goto err;
+ }
+
+ if (EVBUFFER_LENGTH(bufev->output) != 0) {
+ ret = tls_write(conn->ctx,
+ EVBUFFER_DATA(bufev->output),
+ EVBUFFER_LENGTH(bufev->output));
+ switch (ret) {
+ case TLS_WANT_POLLIN:
+ case TLS_WANT_POLLOUT:
+ goto retry;
+ case -1:
+ what |= EVBUFFER_ERROR;
+ goto err;
+ }
+ len = ret;
+ evbuffer_drain(bufev->output, len);
+ }
+
+ if (EVBUFFER_LENGTH(bufev->output) != 0)
+ event_add(&bufev->ev_write, NULL);
+
+ if (bufev->writecb != NULL &&
+ EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low)
+ (*bufev->writecb)(bufev, bufev->cbarg);
+ return;
+
+retry:
+ event_add(&bufev->ev_write, NULL);
+ return;
+
+err:
+ (*bufev->errorcb)(bufev, what, bufev->cbarg);
+}
+
+static void
+setup(void)
+{
+ struct addrinfo hints, *res, *res0;
+ int v, r, saved_errno;
+ char host[64];
+ const char *c, *h, *port, *cause;
+
+ if ((c = strchr(fromhost, ':')) == NULL) {
+ h = NULL;
+ port = fromhost;
+ } else {
+ if ((c = copysec(fromhost, host, sizeof(host))) == NULL)
+ fatalx("hostname too long: %s", fromhost);
+ h = host;
+ port = c+1;
+ }
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_PASSIVE;
+
+ r = getaddrinfo(h, port, &hints, &res0);
+ if (r != 0)
+ fatalx("getaddrinfo(%s): %s", fromhost,
+ gai_strerror(r));
+
+ for (res = res0; res && nsock < MAXSOCK; res = res->ai_next) {
+ socks[nsock] = socket(res->ai_family, res->ai_socktype,
+ res->ai_protocol);
+ if (socks[nsock] == -1) {
+ cause = "socket";
+ continue;
+ }
+
+ if (bind(socks[nsock], res->ai_addr, res->ai_addrlen) == -1) {
+ cause = "bind";
+ saved_errno = errno;
+ close(socks[nsock]);
+ errno = saved_errno;
+ continue;
+ }
+
+ v = 1;
+ if (setsockopt(socks[nsock], SOL_SOCKET, SO_REUSEADDR, &v,
+ sizeof(v)) == -1)
+ err(1, "setsockopt(SO_REUSEADDR)");
+
+ v = 1;
+ if (setsockopt(socks[nsock], SOL_SOCKET, SO_REUSEPORT, &v,
+ sizeof(v)) == -1)
+ err(1, "setsockopt(SO_REUSEPORT)");
+
+ listen(socks[nsock], 5);
+ nsock++;
+ }
+
+ if (nsock == 0)
+ fatal("%s", cause);
+
+ freeaddrinfo(res0);
+}
+
+static int
+servconnect(void)
+{
+ struct addrinfo hints, *res, *res0;
+ int r, saved_errno, sock;
+ const char *cause;
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+
+ r = getaddrinfo(kamihost, kamiport, &hints, &res0);
+ if (r != 0) {
+ log_warnx("getaddrinfo(%s, %s): %s", kamihost, kamiport,
+ gai_strerror(r));
+ return -1;
+ }
+
+ for (res = res0; res != NULL; res = res->ai_next) {
+ sock = socket(res->ai_family, res->ai_socktype,
+ res->ai_protocol);
+ if (sock == -1) {
+ cause = "socket";
+ continue;
+ }
+
+ if (connect(sock, res->ai_addr, res->ai_addrlen) == -1) {
+ cause = "connect";
+ saved_errno = errno;
+ close(sock);
+ errno = saved_errno;
+ sock = -1;
+ continue;
+ }
+
+ /* found one */
+ break;
+ }
+
+ if (sock == -1)
+ log_warn("%s", cause);
+
+ freeaddrinfo(res0);
+ return sock;
+}
+
+static void
+copy_to_server(struct bufferevent *bev, void *d)
+{
+ struct conn *c = d;
+
+ bufferevent_write_buffer(c->server, EVBUFFER_INPUT(bev));
+}
+
+static void
+copy_to_client(struct bufferevent *bev, void *d)
+{
+ struct conn *c = d;
+
+ bufferevent_write_buffer(c->client, EVBUFFER_INPUT(bev));
+}
+
+static void
+nopcb(struct bufferevent *bev, void *d)
+{
+ return;
+}
+
+static void
+errcb(struct bufferevent *bev, short ev, void *d)
+{
+ struct conn *c = d;
+
+ log_debug("closing connection (event=%x / side=%s)", ev,
+ bev == c->server ? "server" : "client");
+
+ bufferevent_free(c->server);
+ bufferevent_free(c->client);
+
+ tls_close(c->ctx);
+ tls_free(c->ctx);
+
+ close(c->lfd);
+ close(c->kfd);
+
+ free(c);
+}
+
+static void
+doaccept(int fd, short ev, void *data)
+{
+ struct tls_config *conf;
+ struct conn *c;
+ int r;
+
+ if ((c = calloc(1, sizeof(*c))) == NULL)
+ fatal("calloc");
+
+ if ((c->lfd = accept(fd, NULL, 0)) == -1) {
+ log_warn("accept");
+ free(c);
+ return;
+ }
+
+ if ((c->kfd = servconnect()) == -1) {
+ close(c->lfd);
+ free(c);
+ return;
+ }
+
+ if ((c->ctx = tls_client()) == NULL)
+ fatal("tls_client");
+
+ if ((conf = tls_config_new()) == NULL)
+ fatal("tls_config_new");
+
+ if (tls_config_set_cert_mem(conf, cert, certlen) == -1 ||
+ tls_config_set_key_mem(conf, key, keylen) == -1)
+ fatalx("tls_config_set_{cert,key}: %s", tls_config_error(conf));
+ tls_config_insecure_noverifycert(conf);
+
+ if (tls_configure(c->ctx, conf) == -1)
+ fatalx("tls_configure");
+
+ tls_config_free(conf);
+
+ if (tls_connect_socket(c->ctx, c->kfd, kamihost) == -1)
+ fatal("tls_connect_socket");
+
+again: switch (r = tls_handshake(c->ctx)) {
+ case -1:
+ log_warnx("tls_handshake: %s", tls_error(c->ctx));
+ tls_close(c->ctx);
+ tls_free(c->ctx);
+ close(c->lfd);
+ close(c->kfd);
+ free(c);
+ return;
+ case TLS_WANT_POLLIN:
+ case TLS_WANT_POLLOUT:
+ goto again;
+ }
+
+ c->server = bufferevent_new(c->kfd, copy_to_client, nopcb, errcb, c);
+ if (c->server == NULL)
+ fatal("bufferevent_new");
+
+ event_set(&c->server->ev_read, c->kfd, EV_READ, tls_readcb,
+ c->server);
+ event_set(&c->server->ev_write, c->kfd, EV_WRITE, tls_writecb,
+ c->server);
+
+#if HAVE_EVENT2
+ evbuffer_unfreeze(c->server->input, 0);
+ evbuffer_unfreeze(c->server->output, 1);
+#endif
+
+ c->client = bufferevent_new(c->lfd, copy_to_server, nopcb, errcb, c);
+ if (c->client == NULL)
+ fatal("bufferevent_new");
+
+ bufferevent_enable(c->server, EV_READ|EV_WRITE);
+ bufferevent_enable(c->client, EV_READ|EV_WRITE);
+}
+
+__dead static void
+usage(void)
+{
+ fprintf(stderr,
+ "usage: %s [-dv] -c host[:port] -l [host:]port -C cert [-K key]\n",
+ getprogname());
+ exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+ int ch, i;
+ const char *certf = NULL, *keyf = NULL;
+
+ log_init(1, LOG_DAEMON);
+ log_setverbose(1);
+
+ while ((ch = getopt(argc, argv, "C:c:dK:l:v")) != -1) {
+ switch (ch) {
+ case 'C':
+ certf = optarg;
+ break;
+ case 'c':
+ tohost = optarg;
+ break;
+ case 'd':
+ debug = 1;
+ break;
+ case 'K':
+ keyf = optarg;
+ break;
+ case 'l':
+ fromhost = optarg;
+ break;
+ case 'v':
+ verbose = 1;
+ break;
+ default:
+ usage();
+ }
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc != 0)
+ usage();
+ if (certf == NULL || tohost == NULL || fromhost == NULL)
+ usage();
+ if (keyf == NULL)
+ keyf = certf;
+
+ parse_tohost();
+
+ if ((cert = tls_load_file(certf, &certlen, NULL)) == NULL)
+ fatal("can't load %s", certf);
+ if ((key = tls_load_file(keyf, &keylen, NULL)) == NULL)
+ fatal("can't load %s", keyf);
+
+ log_init(debug, LOG_DAEMON);
+ log_setverbose(verbose);
+
+ if (!debug)
+ daemon(1, 0);
+
+ signal(SIGPIPE, SIG_IGN);
+
+ event_init();
+
+ setup();
+ for (i = 0; i < nsock; ++i) {
+ event_set(&sockev[i], socks[i], EV_READ|EV_PERSIST,
+ doaccept, NULL);
+ event_add(&sockev[i], NULL);
+ }
+
+ if (pledge("stdio dns inet", NULL) == -1)
+ err(1, "pledge");
+
+ log_info("starting");
+ event_dispatch();
+
+ return 0;
+}