/* * * sst: Simple Ssl Tunneling. * * - yet another generic SSL client/server/forwarder. * * - can be used as a drop-in program for encoding * or decoding networked I/O. * * - can be used as an SSL preprocessor/decoder for inetd servers. * * NOTE: * netcat (nc) is needed to handle networking I/O * when operating as a standalone client, as a * standalone server, or as an inetd forwarder. * *** * * Written by P Kern . * * Copyright (c) 2000 University of Toronto. All rights reserved. * Anyone may use or copy this software, except that it may not be * sold for profit, that this copyright notice remain intact, and that * credit is given where it is due. The University of Toronto and the * author make no warranty and accept no liability for this software. * *** * * Partly inspired by: * * stunnel Universal SSL tunnel * Copyright (c) 1998-2000 Michal Trojnara * All Rights Reserved * *** * * SSL code usage based on openssl/demos/ssl/{cli,serv}.cpp: * * from cli.cpp: * /* cli.cpp - Minimal ssleay client for Unix * 30.9.1996, Sampo Kellomaki ** * /* mangled to work with SSLeay-0.9.0b and OpenSSL 0.9.2b * Simplified to be even more minimal * 12/98 - 4/99 Wade Scholine ** * * from serv.cpp: * /* serv.cpp - Minimal ssleay server for Unix * 30.9.1996, Sampo Kellomaki ** * /* mangled to work with SSLeay-0.9.0b and OpenSSL 0.9.2b * Simplified to be even more minimal * 12/98 - 4/99 Wade Scholine ** * *** */ /***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example usages: ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 1. * * sst -c * ------ * +-----------+ * unencrypted stream | | SSL encrypted stream * <======================>+fd0 fd1+<======================> * | | * +-----------+ * * - relay between unencrypted data on stdin * and SSL encrypted data on stdout. * - expects the remote side to have certificates in order. * - NOTE: not intended for interactive use since shells tend to * create stdin and stdout as unidirectional file-descriptors. * in this mode, sst expects to be able to both read and write * on either descriptor. * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 2. * * sst -s * ------ * +-----------+ * SSL encrypted stream | | decrypted stream * <======================>+fd0 fd1+<======================> * | | * +-----------+ * * - relay between SSL encrypted data on stdin * and decrypted raw data on stdout * - try to have the certificates in order. * - NOTE: not intended for interactive use since shells tend to * create stdin and stdout as unidirectional file-descriptors. * in this mode, sst expects to be able to both read and write * on either descriptor. * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 3. * * sst -c -p host:NNN * ------------------ * +-----------+ * unencrypted stream | | decrypted stream * >>-------------------->>+fd0 fd1+>>--------------------->> * | | * | | (parent process) * +-----+-----+ * ^ * | SSL encrypted stream * | * v * +-----+-----+ * (child process) | fd0/fd1 | * | | network data stream * | +<======================> * "nc host NNN" | | * +-----------+ * * - same as example 1 but use netcat to establish the * network connection to host hhhh at port NNN on the remote side. * - suitable for interactive use/testing. * - sample usages: * % sst -c -p mbox.ca:993 # interact with a remote IMAP/SSL server * % sst -c -p 995 # interact with the local POP/SSL server * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 4. * * sst -s -p NNN * ------------- * +-----------+ * unencrypted stream | | decrypted stream * >>-------------------->>+fd0 fd1+>>--------------------->> * | | * (parent process) | | * +-----+-----+ * ^ * | SSL encrypted stream * | * v * +-----+-----+ * | fd0/fd1 | (child process) * network data stream | | * <======================>+ | * | | "nc -l -p NNN" * +-----------+ * * - same as example 2 but use netcat to listen * for network connections on port NNN. * - suitable for interactive use/testing. * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 5. * * sst -i -p hhhh:NNN * ------------------ * +-----------+ * SSL encrypted stream | | * <======================>+fd0 | * [inherited from inetd] | | (parent process) * | fd1 | * +-----+-----+ * ^ * | decrypted stream * v * +-----+-----+ * (child process) | fd0/fd1 | * | | network data stream * | +<======================> * "nc hhhh NNN" | | * +-----------+ * * - inetd mode ("-i") implies server mode ("-s") as with example 2. * - forwards (punts) the decrypted stream to another host (hhhh) and * port (NNN) by using netcat to establish the network connection. * * - sample inetd.conf entries: * * # handle SSL encryption for our local imap server. * simap stream tcp nowait root /local/bin/sst sst -i -p 143 * * or * * # handle SSL encryption for the imap server on mboxhost. * simap stream tcp nowait root /local/bin/sst sst -i -p mboxhost:143 * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** * example 6. * * sst -i -- * ----------------------- * +-----------+ * SSL encrypted stream | | * <======================>+fd0 | * [inherited from inetd] | | (parent process) * | fd1 | * +-----+-----+ * ^ * | decrypted stream * v * +-----+-----+ * (child process) | fd0/fd1 | * | | * "" | | * +-----------+ * - as with example 5 but instead of running netcat, just execute an * arbitrary command to use/process the data on the decrypted stream. * * - a sample inetd.conf entry: * * # encrypted quotes - make fortunes available via SSL. * qotd stream tcp nowait root /local/bin/sst sst -i -- /usr/games/fortune -l * ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** ***** */ #ifndef lint static char rcsid[] = "$Header: /local/src/local.bin/sst/SRC/RCS/sst.c,v 1.15 2001/04/10 20:20:50 pkern Exp $"; #endif #include #include #include #include #include #include #include /* for MAXPATHLEN */ #include #include #include #include #include #include #if defined(sun) || defined(__FreeBSD__) #include #endif #include #ifndef AF_LOCAL #define AF_LOCAL AF_UNIX #endif /* #include /* SSLeay stuff */ #include #include #include #include #include #define Perror(s) \ if (logging) syslog(LOG_ERR, "%s: %m", (s)); else perror((s)); #define CHK_NULL(x) if ((x)==NULL) quit (1) #define CHK_ERR(err,s) if ((err)==-1) { Perror(s); quit(2); } #define CHK_SSL(err) if ((err)==-1) { show_SSL_errors(); quit(3); } int server = 0; int debug = 0; int verbose = 0; int logging = 0; int timeout = 0; int inetd = 0; int eofclnt = 0; char *prog = "sst"; char *host = NULL; char *port = NULL; char *method = NULL; char certfbuf[MAXPATHLEN], ssldbuf[MAXPATHLEN]; char *certf = NULL, *pkeyf = NULL, *ssld = NULL; #ifndef CONFDIR #define CONFDIR "/local/lib/openssl" #endif #ifndef CERTF #define CERTF "certs/sst.pem" #endif #ifndef LOG_SSL #define LOG_SSL LOG_USER #endif #ifndef NETCAT #define NETCAT "/local/bin/nc" #endif #ifdef USE_EGD /* * USE_EGD - added for openssl-0.9.6a. * according to OpenSSL's FAQs, the need for this extra USE_EGD code * will be made obsolete because the RAND_* routines in openssl-0.9.7 * will automatically search for EGD sockets at pre-defined locations. */ #ifndef EGD_SOCK #define EGD_SOCK "/tmp/.egd-pool" #endif char *egds = EGD_SOCK; #endif /* USE_EGD */ FILE *tty = NULL; pid_t pid = 0; /* * ERR_log_errors(): * adapted from ERR_print_errors_fp() * as found in OpenSSL's ... * crypto/err/err_prn.c * Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. */ void ERR_log_errors() { unsigned long l; char buf[200]; const char *file,*data; int line,flags; unsigned long es; es=CRYPTO_thread_id(); while ((l=ERR_get_error_line_data(&file,&line,&data,&flags)) != 0) { syslog(LOG_ERR,"%lu:%s:%s:%d:%s\n", es,ERR_error_string(l,buf),file,line, (flags&ERR_TXT_STRING)?data:""); } } void show_SSL_errors() { if (logging) ERR_log_errors(); else ERR_print_errors_fp(stderr); } #define SHOW_x(L,F,x) { \ if (logging) syslog((L), "%s", (x)); \ else fprintf((F), "%d: %s\n", getpid(), (x)); } #define SHOW_x1(L,F,M,x) { \ if (logging) syslog((L), (M), (x)); \ else { fprintf((F), "%d: ", getpid()); \ fprintf((F), (M), (x)); fputs("\n", (F)); } } #define SHOW_x2(L,F,M,x1,x2) { \ if (logging) syslog((L), (M), (x1), (x2)); \ else { fprintf((F), "%d: ", getpid()); \ fprintf((F), (M), (x1), (x2)); fputs("\n", (F)); } } #define SHOW_err(a) SHOW_x(LOG_ERR,stderr,a) #define SHOW_err1(f,a) SHOW_x1(LOG_ERR,stderr,f,a) #define SHOW_info(a) SHOW_x(LOG_DEBUG,tty,a) #define SHOW_info1(f,a) SHOW_x1(LOG_DEBUG,tty,f,a) #define SHOW_info2(f,a1,a2) SHOW_x2(LOG_DEBUG,tty,f,a1,a2) char *usageopts[] = { "", " options:", " --------", " -c = client mode.", " -s = server mode.", " -e = shutdown on client's EOF.", " -l = redirect all messages to syslog(3).", " -i = inetd mode (implies both '-s' and '-l').", " -v = be chatty about what's happening.", " -d = enable debugging messages (this implies '-v').", " repeated use of '-d' enables more debugging output.", " -p [host:]port = connect to (or listen to) portnum.", " {client,inetd - assumes 'localhost' if no 'host:' part given}", " {server - 'host:' part is ignored if it's present }", " -t timeout = set maximum idle time (in seconds).", " -C cert-file = use instead of the default certificate.", " -K pkey-file = use instead of the default private key file.", " -D ssl-conf = use as the path to default cert/keys.", " -M method = use a specific SSL method (ssl2, ssl3 or tls1).", #ifdef USE_EGD " -E skt-path = use instead of the default EGD socket.", #endif "", " auxiliary command:", " ------------------", " the command to be executed instead of the default netcat commands.", "", NULL }; usage() { char **uop = usageopts; if (logging) { syslog(LOG_ERR, "usage: %s [ '--' ]", prog); while (*uop != NULL) syslog(LOG_ERR, "%s", *uop++); } else { fprintf(stderr, "usage: %s [ '--' ]\n", prog); while (*uop != NULL) fprintf(stderr, "%s\n", *uop++); } } /* reaper -- zombie prevention */ void reaper() { int w; pid_t p; while ((p = wait3(&w, WNOHANG, 0)) > 0) { if (debug) SHOW_info1("reaped %d", p); if (p == pid) pid = 0; continue; } } /* quit -- clean up and exit */ static void quit(n) int n; { if (debug) SHOW_info1("quit %d", n); if (pid > 0) { if (debug) SHOW_info1("kill %d", pid); (void) kill (pid, SIGKILL); } exit(n); } /* * relay data. * * - decrypt data recvd on the SSL descriptor (sd) and * write the resulting output to the wd descriptor. * * - read data recvd on the rd descriptor, and send it out, via * SSL_write, to be encrypted and written to the sd descriptor. * * - EOF on the SSL stream means that the SSL connection has shutdown. * * - EOF on rd when in server mode means the actual server has finished. */ relay(ssl, sd, rd, wd) SSL *ssl; int sd, rd, wd; { fd_set fds; char *p, buf[4096]; int err, r, w = getdtablesize(); struct timeval tv, *tp = NULL; off_t nsr, nsw, nlr, nlw; int n, nw, rf = 1, sf = 1; nsr = nsw = nlr = nlw = (off_t)0; if (timeout > 0) { tp = &tv; tv.tv_sec = timeout; tv.tv_usec = 0; } while (sf || rf) { FD_ZERO(&fds); if (sf) FD_SET(sd, &fds); if (rf) FD_SET(rd, &fds); r = select(w, &fds, NULL, NULL, tp); if (debug > 3) SHOW_info1("select = %d", r); if (r < 0) { if (errno == EINTR) { /* Interrupted system call */ if (debug) Perror("select"); } else { Perror("select"); quit(10); } } else if (r == 0) { if (verbose) SHOW_info("timed out."); goto done; } else for ( ; r > 0; r--) { if (FD_ISSET(sd, &fds)) { if (debug > 3) SHOW_info1("select: sd: %d", r); err = SSL_read(ssl, buf, sizeof(buf) - 1); CHK_SSL(err); buf[err] = '\0'; if (err == 0) { sf = 0; if (debug) SHOW_info("EOF(sd)"); goto done; } nsr += err; for (p = buf, nw = err; nw > 0; nw -= n, p += n) { n = write(wd, p, nw); if (n < 0) { Perror("local write"); quit(12); } } nlw += err; FD_CLR(sd, &fds); continue; } if (FD_ISSET(rd, &fds)) { if (debug > 3) SHOW_info1("select: rd: %d", r); n = read(rd, buf, sizeof(buf)-1); if (n < 0) { Perror("local read"); quit(13); } if (n == 0) { rf = 0; if (debug) SHOW_info("EOF(rd)"); if (!server && eofclnt) { err = SSL_shutdown(ssl); if (verbose) SHOW_info1("shutdown ssl client (%d)", err); } else if (server && sf) { err = SSL_shutdown(ssl); if (verbose) SHOW_info1("shutdown ssl server (%d)", err); if (err) { sf = 0; if (verbose) SHOW_info("close relay"); } } } else { nlr += n; err = SSL_write(ssl, buf, n); CHK_SSL(err); nsw += err; } FD_CLR(rd, &fds); continue; } } } done: if (sf) { err = SSL_shutdown(ssl); if (verbose) SHOW_info1("shutdown ssl (%d)", err); } if (verbose) { if (sizeof(off_t) > 4) { SHOW_info1("bytes from ssl: %qd", nsr); SHOW_info1("bytes to ssl: %qd", nsw); SHOW_info1("bytes from local: %qd", nlr); SHOW_info1("bytes to local: %qd", nlw); } else { SHOW_info1("bytes from ssl: %ld", nsr); SHOW_info1("bytes to ssl: %ld", nsw); SHOW_info1("bytes from local: %ld", nlr); SHOW_info1("bytes to local: %ld", nlw); } } } srvr_prep(ctx, ssl, sd) SSL_CTX **ctx; SSL **ssl; int sd; { int err; SSL_METHOD *meth; X509 *client_cert; /* * SSL preliminaries: * keep the certificate and key with the context. */ SSL_load_error_strings(); SSLeay_add_ssl_algorithms(); if (method == NULL) meth = SSLv23_server_method(); else if (strcmp(method, "ssl2") == 0) meth = SSLv2_server_method(); else if (strcmp(method, "ssl3") == 0) meth = SSLv3_server_method(); else if (strcmp(method, "tls1") == 0) meth = TLSv1_server_method(); else meth = SSLv23_server_method(); *ctx = SSL_CTX_new (meth); if (!*ctx) { show_SSL_errors(); quit(2); } if (SSL_CTX_use_certificate_file(*ctx, certf, SSL_FILETYPE_PEM) <= 0) { show_SSL_errors(); quit(20); } if (SSL_CTX_use_PrivateKey_file(*ctx, pkeyf, SSL_FILETYPE_PEM) <= 0) { show_SSL_errors(); quit(21); } if (!SSL_CTX_check_private_key(*ctx)) { SHOW_err("private key does not match the certificate public key"); quit(22); } #ifdef USE_EGD if (RAND_egd(egds) == -1) { SHOW_err1("failed to contact EGD via %s", egds); quit(23); } #endif *ssl = SSL_new (*ctx); CHK_NULL(ssl); SSL_set_fd (*ssl, sd); err = SSL_accept (*ssl); CHK_SSL(err); if (verbose) { SHOW_info1("cipher: [%s]", SSL_get_cipher (*ssl)); /* * Get client's certificate * (note: beware of dynamic allocation) */ client_cert = SSL_get_peer_certificate (*ssl); if (client_cert == NULL) { SHOW_info("no client certificate."); } else { char *subj, *issu; subj = X509_NAME_oneline (X509_get_subject_name (client_cert), 0, 0); CHK_NULL(subj); issu = X509_NAME_oneline (X509_get_issuer_name (client_cert), 0, 0); CHK_NULL(issu); SHOW_info1("client cert subject: %s", subj); SHOW_info1("client cert issuer: %s", issu); CRYPTO_free(subj); CRYPTO_free(issu); /* * XXX ... * We could do all sorts of certificate * verification stuff here before * deallocating the certificate. */ X509_free (client_cert); } } } clnt_prep(ctx, ssl, sd) SSL_CTX **ctx; SSL **ssl; int sd; { int err; SSL_METHOD *meth; X509 *server_cert; /* * SSL preliminaries: * keep the certificate and key with the context. */ SSL_load_error_strings(); SSLeay_add_ssl_algorithms(); if (method == NULL) meth = SSLv23_client_method(); else if (strcmp(method, "ssl2") == 0) meth = SSLv2_client_method(); else if (strcmp(method, "ssl3") == 0) meth = SSLv3_client_method(); else if (strcmp(method, "tls1") == 0) meth = TLSv1_client_method(); else meth = SSLv23_client_method(); *ctx = SSL_CTX_new (meth); if (!*ctx) { show_SSL_errors(); quit(2); } #if 0 if (certf && SSL_CTX_use_certificate_file(*ctx, certf, SSL_FILETYPE_PEM) <= 0) { show_SSL_errors(); quit(30); } if (pkeyf && SSL_CTX_use_PrivateKey_file(*ctx, pkeyf, SSL_FILETYPE_PEM) <= 0) { show_SSL_errors(); quit(31); } if (!SSL_CTX_check_private_key(*ctx)) { SHOW_err("private key does not match the certificate public key"); quit(32); } #endif #ifdef USE_EGD if (RAND_egd(egds) == -1) { SHOW_err1("failed to contact EGD via %s", egds); quit(33); } #endif *ssl = SSL_new (*ctx); CHK_NULL(*ssl); SSL_set_fd (*ssl, sd); err = SSL_connect (*ssl); CHK_SSL(err); server_cert = SSL_get_peer_certificate (*ssl); if (server_cert == NULL) { SHOW_err("no server certificate."); quit(33); } if (verbose) { char *subj, *issu; SHOW_info1("cipher: [%s]", SSL_get_cipher (*ssl)); /* * Get server's certificate * (note: beware of dynamic allocation) */ subj = X509_NAME_oneline (X509_get_subject_name (server_cert), 0, 0); CHK_NULL(subj); issu = X509_NAME_oneline (X509_get_issuer_name (server_cert), 0, 0); CHK_NULL(issu); SHOW_info1("server cert subject: %s", subj); SHOW_info1("server cert issuer: %s", issu); CRYPTO_free(subj); CRYPTO_free(issu); /* * XXX ... * We could do all sorts of certificate * verification stuff here before * deallocating the certificate. */ } X509_free (server_cert); } main(ac, av) int ac; char *av[]; { SSL *ssl; SSL_CTX *ctx; int sd = -1, rd, wd; extern char *optarg; extern int optind; int ch, errflg = 0; rd = fileno(stdin); wd = fileno(stdout); prog = av[0]; while ((ch = getopt(ac, av, #ifdef USE_EGD "cdeilqsvp:t:C:K:D:M:E:" #else "cdeilqsvp:t:C:K:D:M:" #endif )) != -1) { switch(ch) { case 'c': server = 0; break; case 's': server = 1; break; case 'd': debug++; break; case 'e': eofclnt = 1; break; case 'i': inetd = 1; break; case 'p': port = optarg; break; case 'q': verbose = 0; break; case 'v': verbose = 1; break; case 'l': logging = 1; break; case 't': timeout = atoi(optarg); break; case 'K': pkeyf = optarg; break; case 'C': certf = optarg; break; case 'D': ssld = optarg; break; case 'M': method = optarg; break; #ifdef USE_EGD case 'E': egds = optarg; break; #endif default: errflg = 1; break; } } if (inetd) logging = 1, server = 1; if (logging) openlog(prog, LOG_PID, LOG_SSL); if (errflg) { usage: usage(); quit(1); } if (ssld == NULL) { strcpy(ssldbuf, CONFDIR); ssld = ssldbuf; } if (certf == NULL) { strcpy(certfbuf, ssld); strcat(certfbuf, "/"); strcat(certfbuf, CERTF); certf = certfbuf; } if (pkeyf == NULL) pkeyf = certf; if (debug) verbose = debug; if (tty == NULL) tty = stderr; if (port != NULL) { char *cp = (char *)index(port, ':'); if (cp == NULL) host = "127.0.0.1"; else { host = port; *cp++ = '\0'; port = cp; } } if (verbose) { SHOW_info1("base: %s", ssld); SHOW_info1("cert: %s", certf); SHOW_info1("pkey: %s", pkeyf); #ifdef USE_EGD SHOW_info1("EGD sock: %s", egds); #endif if (host != NULL) SHOW_info1("punt host: %s", host); if (port != NULL) SHOW_info1("punt port: %s", port); if (timeout > 0) SHOW_info1("timeout: %d", timeout); } /* * if an additional command was specified or if a host/port was * given (which means that netcat will be used to handle the raw * network I/O), then create the socket/filedescriptors which * will be used to communicate with the child process and then * fork+exec that child process. */ if (optind < ac || port != NULL) { int sv[2]; if (socketpair(AF_LOCAL, SOCK_STREAM, PF_UNSPEC, sv) < 0) { Perror("socketpair()"); quit(1); } signal(SIGCHLD, reaper); /* zombie prevention */ pid = fork(); if (pid < 0) { Perror("fork()"); quit(1); } if (pid == 0) { /* child */ if (dup2(sv[1], 0) < 0) Perror("dup2(0)"); if (dup2(sv[1], 1) < 0) Perror("dup2(1)"); for (sd = getdtablesize(); sd > 2; sd--) (void) close(sd); if (verbose && logging) openlog(prog, LOG_PID, LOG_SSL); if (optind < ac) { av += optind; if (verbose) { SHOW_info1("exec '%s ...'", av[0]); if (logging) closelog(); } execvp(av[0], av); } else if (port && server && !inetd) { if (verbose) { SHOW_info1("exec 'nc -l -p %s'", port); if (logging) closelog(); } execl(NETCAT, "nc", "-l", "-p", port, NULL); } else if (port) { if (verbose) { SHOW_info2("exec 'nc %s %s'", host, port); if (logging) closelog(); } execl(NETCAT, "nc", host, port, NULL); } if (logging) openlog(prog, LOG_PID, LOG_SSL); Perror("execl/execvp"); _exit(1); } /* parent */ if (debug) SHOW_info1("launched %d", pid); if (inetd) { if (debug > 2) { SHOW_info1("rd %d", rd); SHOW_info1("wd %d", wd); SHOW_info1("sd %d", sd); SHOW_info1("sv[0] %d", sv[0]); SHOW_info1("sv[1] %d", sv[1]); } sd = rd; rd = wd = sv[0]; } else sd = sv[0]; close(sv[1]); } if (server) { if (sd < 0) { /* see example 2. */ sd = fileno(stdin); rd = wd; } srvr_prep(&ctx, &ssl, sd); } else { if (sd < 0) { /* see example 1. */ sd = fileno(stdout); wd = rd; } clnt_prep(&ctx, &ssl, sd); } if (debug > 2) { SHOW_info1("rd: %d", rd); SHOW_info1("wd: %d", wd); SHOW_info1("sd: %d", sd); } relay(ssl, sd, rd, wd); close (sd); SSL_free (ssl); SSL_CTX_free (ctx); if (pid > 0) (void) kill (pid, SIGKILL); quit(0); }