From 9bcb2cdd7a920ebc78b59d0b5797d678424aa93a Mon Sep 17 00:00:00 2001 From: Yves Rutschle Date: Wed, 10 Jul 2013 23:15:38 +0200 Subject: [PATCH] v1.12: 08MAY2012 Added support for configuration file. New protocol probes can be defined using regular expressions that match the first packet sent by the client. sslh now connects timed out connections to the first configured protocol instead of 'ssh' (just make sure ssh is the first defined protocol). sslh now tries protocols in the order in which they are defined (just make sure sslh is the last defined protocol). --- ChangeLog | 15 ++ Makefile | 11 +- README | 19 ++- common.c | 250 +++++--------------------------- common.h | 39 ++--- example.cfg | 40 ++++++ probe.c | 216 ++++++++++++++++++++++++++++ probe.h | 54 +++++++ sslh-fork.c | 17 +-- sslh-main.c | 389 +++++++++++++++++++++++++++++++++++++++++++------- sslh-select.c | 20 +-- sslh.pod | 50 +++++-- t | 4 +- t_load | 2 +- 14 files changed, 794 insertions(+), 332 deletions(-) create mode 100644 example.cfg create mode 100644 probe.c create mode 100644 probe.h diff --git a/ChangeLog b/ChangeLog index 8c71824..1c62975 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,18 @@ +v1.12: 08MAY2012 + Added support for configuration file. + + New protocol probes can be defined using regular + expressions that match the first packet sent by the + client. + + sslh now connects timed out connections to the first + configured protocol instead of 'ssh' (just make sure + ssh is the first defined protocol). + + sslh now tries protocols in the order in which they + are defined (just make sure sslh is the last defined + protocol). + v1.11: 21APR2012 WARNING: defaults have been removed for --user and --pidfile options, update your start-up scripts! diff --git a/Makefile b/Makefile index 567d6ae..5d0370d 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Configuration -VERSION="v1.11" +VERSION="v1.12" +USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) USELIBWRAP= # Use libwrap? COV_TEST= # Perform test coverage? PREFIX=/usr/local @@ -17,15 +18,19 @@ endif CC = gcc CFLAGS=-Wall -g $(CFLAGS_COV) -#LIBS=-lnet LIBS= -OBJS=common.o sslh-main.o +OBJS=common.o sslh-main.o probe.o ifneq ($(strip $(USELIBWRAP)),) LIBS:=$(LIBS) -lwrap CFLAGS:=$(CFLAGS) -DLIBWRAP endif +ifneq ($(strip $(USELIBCONFIG)),) + LIBS:=$(LIBS) -lconfig + CFLAGS:=$(CFLAGS) -DLIBCONFIG +endif + all: sslh $(MAN) echosrv .c.o: *.h diff --git a/README b/README index 1b3f1d6..d17a1e3 100644 --- a/README +++ b/README @@ -1,17 +1,30 @@ ===== sslh -- A ssl/ssh multiplexer. ===== -sslh accepts HTTP, HTTPS, SSH, OpenVPN, tinc and XMPP -connections on the same port. This makes it possible to -connect to any of these servers on port 443 (e.g. from +sslh accepts connections in HTTP, HTTPS, SSH, OpenVPN, +tinc, XMPP, or any other protocol that can be tested using a +regular expression, on the same port. This makes it possible +to connect to any of these servers on port 443 (e.g. from inside a corporate firewall, which almost never block port 443) while still serving HTTPS on that port. + ==== Compile and install ==== If you're lucky, the Makefile will work for you: make install +There are a couple of configuration options at the beginning +of the Makefile: + + USELIBWRAP compiles support for host access control (see + hosts_access(3)), you will need libwrap headers and + library to compile (libwrap0-dev in Debian). + + USELIBCONFIG compiles support for the configuration + file. You will need libconfig headers to compile + (libconfig8-dev in Debian). + The Makefile produces two different executables: sslh-fork and sslh-select. diff --git a/common.c b/common.c index 90f7018..36338f1 100755 --- a/common.c +++ b/common.c @@ -5,23 +5,7 @@ **/ #define _GNU_SOURCE -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include "common.h" @@ -31,34 +15,6 @@ #define SA_NOCLDWAIT 0 #endif -int is_ssh_protocol(const char *p, int len); -int is_openvpn_protocol(const char *p, int len); -int is_tinc_protocol(const char *p, int len); -int is_xmpp_protocol(const char *p, int len); -int is_http_protocol(const char *p, int len); -int is_true(const char *p, int len) { return 1; } - -/* Table of all the protocols we know how to connect to. - * - * The first protocol in the table is where we connect in case of timeout - * (client didn't speak: typically this is SSH.) - * - * The last protocol in the table is where we connect if client spoke but we - * couldn't probe what it's saying. - */ -struct proto protocols[] = { - /* affected description service saddr probe */ - { 0, "ssh", "sshd", {0}, is_ssh_protocol }, - { 0, "openvpn", NULL, {0}, is_openvpn_protocol }, - { 0, "tinc", NULL, {0}, is_tinc_protocol }, - { 0, "xmpp", NULL, {0}, is_xmpp_protocol }, - { 0, "http", NULL, {0}, is_http_protocol }, - /* probe for SSL always successes: it's the default, and must be tried last - **/ - { 0, "ssl", NULL, {0}, is_true } -}; -int num_known_protocols = ARRAY_SIZE(protocols); - /* * Settings that depend on the command line. They're set in main(), but also * used in other places in common.c, and it'd be heavy-handed to pass it all as @@ -69,7 +25,7 @@ int probing_timeout = 2; int inetd = 0; int foreground = 0; int numeric = 0; -char *user_name, *pid_file; +const char *user_name, *pid_file, *rule_filename; struct addrinfo *addr_listen = NULL; /* what addresses do we listen to? */ @@ -141,7 +97,7 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list) /* Connect to first address that works and returns a file descriptor, or -1 if * none work. cnx_name points to the name of the service (for logging) */ -int connect_addr(struct addrinfo *addr, char* cnx_name) +int connect_addr(struct addrinfo *addr, const char* cnx_name) { struct addrinfo *a; char buf[NI_MAXHOST]; @@ -291,123 +247,6 @@ int fd2fd(struct queue *target_q, struct queue *from_q) return size_w; } -/* If the client wrote something first, read it and check if it's a SSH banner. - * Data is left in appropriate defered write buffer. - */ -int is_ssh_protocol(const char *p, int len) -{ - if (!strncmp(p, "SSH-", 4)) { - return 1; - } - return 0; -} - -/* Is the buffer the beginning of an OpenVPN connection? - * (code lifted from OpenVPN port-share option) - */ -int is_openvpn_protocol (const char*p,int len) -{ -#define P_OPCODE_SHIFT 3 -#define P_CONTROL_HARD_RESET_CLIENT_V2 7 - if (len >= 3) - { - return p[0] == 0 - && p[1] >= 14 - && p[2] == (P_CONTROL_HARD_RESET_CLIENT_V2<= 2) - { - return p[0] == 0 && p[1] >= 14; - } - else - return 0; -} - -/* Is the buffer the beginning of a tinc connections? - * (protocol is undocumented, but starts with "0 " in 1.0.15) - * */ -int is_tinc_protocol( const char *p, int len) -{ - return !strncmp(p, "0 ", 2); -} - -/* Is the buffer the beginning of a jabber (XMPP) connections? - * (Protocol is documented (http://tools.ietf.org/html/rfc6120) but for lazy - * clients, just checking first frame containing "jabber" in xml entity) - * */ -int is_xmpp_protocol( const char *p, int len) -{ - return strstr(p, "jabber") ? 1 : 0; -} - -int probe_http_method(const char *p, const char *opt) -{ - return !strncmp(p, opt, strlen(opt)-1); -} - -/* Is the buffer the beginnin of an HTTP connection? */ -int is_http_protocol(const char *p, int len) -{ - /* If it's got HTTP in the request (HTTP/1.1) then it's HTTP */ - if (strstr(p, "HTTP")) - return 1; - - /* Otherwise it could be HTTP/1.0 without version: check if it's got an - * HTTP method (RFC2616 5.1.1) */ - probe_http_method(p, "OPTIONS"); - probe_http_method(p, "GET"); - probe_http_method(p, "HEAD"); - probe_http_method(p, "POST"); - probe_http_method(p, "PUT"); - probe_http_method(p, "DELETE"); - probe_http_method(p, "TRACE"); - probe_http_method(p, "CONNECT"); - - return 0; -} - - -/* - * Read the beginning of data coming from the client connection and check if - * it's a known protocol. Then leave the data on the defered - * write buffer of the connection and returns the protocol index in the - * protocols[] array * - */ -T_PROTO_ID probe_client_protocol(struct connection *cnx) -{ - char buffer[BUFSIZ]; - int n, i; - - n = read(cnx->q[0].fd, buffer, sizeof(buffer)); - /* It's possible that read() returns an error, e.g. if the client - * disconnected between the previous call to select() and now. If that - * happens, we just connect to the default protocol so the caller of this - * function does not have to deal with a specific failure condition (the - * connection will just fail later normally). */ - if (n > 0) { - defer_write(&cnx->q[1], buffer, n); - - for (i = 0; i < ARRAY_SIZE(protocols); i++) { - if (protocols[i].affected) { - if (protocols[i].probe(buffer, n)) { - return i; - } - } - } - } - - /* If none worked, return the first one affected (that's completely - * arbitrary) */ - for (i = 0; i < ARRAY_SIZE(protocols); i++) - if (protocols[i].affected) - return i; - - /* At this stage... nothing is affected. This shouldn't happen as we check - * at least one target exists when we parse the commnand line */ - fprintf(stderr, "FATAL: No protocol affected. This should not happen.\n"); - exit(1); -} - /* returns a string that prints the IP and port of the sockaddr */ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) { @@ -439,13 +278,30 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) return buf; } +/* Turns a hostname and port (or service) into a list of struct addrinfo + * returns 0 on success, -1 otherwise and logs error + **/ +int resolve_split_name(struct addrinfo **out, const char* host, const char* serv) +{ + struct addrinfo hint; + int res; + + memset(&hint, 0, sizeof(hint)); + hint.ai_family = PF_UNSPEC; + hint.ai_socktype = SOCK_STREAM; + + res = getaddrinfo(host, serv, &hint, out); + if (res) + log_message(LOG_ERR, "%s `%s:%s'\n", gai_strerror(res), host, serv); + return res; +} + /* turns a "hostname:port" string into a list of struct addrinfo; out: list of newly allocated addrinfo (see getaddrinfo(3)); freeaddrinfo(3) when done fullname: input string -- it gets clobbered */ void resolve_name(struct addrinfo **out, char* fullname) { - struct addrinfo hint; char *serv, *host; int res; @@ -453,7 +309,7 @@ void resolve_name(struct addrinfo **out, char* fullname) if (!sep) /* No separator: parameter is just a port */ { - fprintf(stderr, "names must be fully specified as hostname:port\n"); + fprintf(stderr, "%s: names must be fully specified as hostname:port\n", fullname); exit(1); } @@ -461,11 +317,7 @@ void resolve_name(struct addrinfo **out, char* fullname) serv = sep+1; *sep = 0; - memset(&hint, 0, sizeof(hint)); - hint.ai_family = PF_UNSPEC; - hint.ai_socktype = SOCK_STREAM; - - res = getaddrinfo(host, serv, &hint, out); + res = resolve_split_name(out, host, serv); if (res) { fprintf(stderr, "%s `%s'\n", gai_strerror(res), fullname); if (res == EAI_SERVICE) @@ -533,7 +385,7 @@ void log_connection(struct connection *cnx) * * Returns -1 if access is denied, 0 otherwise */ -int check_access_rights(int in_socket, char* service) +int check_access_rights(int in_socket, const char* service) { #ifdef LIBWRAP struct sockaddr peeraddr; @@ -572,7 +424,6 @@ int check_access_rights(int in_socket, char* service) return 0; } - void setup_signals(void) { int res; @@ -586,18 +437,23 @@ void setup_signals(void) res = sigaction(SIGCHLD, &action, NULL); CHECK_RES_DIE(res, "sigaction"); - /* Set SIGTERM to exit. For some reason if it's not set explicitely, * coverage information is lost when killing the process */ memset(&action, 0, sizeof(action)); action.sa_handler = exit; res = sigaction(SIGTERM, &action, NULL); CHECK_RES_DIE(res, "sigaction"); + + /* Ignore SIGPIPE . */ + action.sa_handler = SIG_IGN; + res = sigaction(SIGPIPE, &action, NULL); + CHECK_RES_DIE(res, "sigaction"); + } /* Open syslog connection with appropriate banner; * banner is made up of basename(bin_name)+"[pid]" */ -void setup_syslog(char* bin_name) { +void setup_syslog(const char* bin_name) { char *name1, *name2; name1 = strdup(bin_name); @@ -610,7 +466,7 @@ void setup_syslog(char* bin_name) { } /* We don't want to run as root -- drop priviledges if required */ -void drop_privileges(char* user_name) +void drop_privileges(const char* user_name) { int res; struct passwd *pw = getpwnam(user_name); @@ -628,7 +484,7 @@ void drop_privileges(char* user_name) } /* Writes my PID */ -void write_pid_file(char* pidfile) +void write_pid_file(const char* pidfile) { FILE *f; @@ -642,45 +498,3 @@ void write_pid_file(char* pidfile) fclose(f); } -void printsettings(void) -{ - char buf[NI_MAXHOST]; - struct addrinfo *a; - int i; - - for (i = 0; i < ARRAY_SIZE(protocols); i++) { - if (protocols[i].affected) - fprintf(stderr, - "%s addr: %s. libwrap service: %s family %d %d\n", - protocols[i].description, - sprintaddr(buf, sizeof(buf), &protocols[i].saddr), - protocols[i].service, - protocols[i].saddr.ai_family, - protocols[i].saddr.ai_addr->sa_family); - } - fprintf(stderr, "listening on:\n"); - for (a = addr_listen; a; a = a->ai_next) { - fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a)); - } - fprintf(stderr, "timeout to ssh: %d\n", probing_timeout); -} - -/* Adds protocols to the list of options, so command-line parsing uses the - * protocol definition array - * options: array of options to add to; must be big enough - * n_opts: number of options in *options before calling (i.e. where to append) - * prot: array of protocols - * n_prots: number of protocols in *prot - * */ -void append_protocols(struct option *options, int n_opts, struct proto *prot, int n_prots) -{ - int o, p; - - for (o = n_opts, p = 0; p < n_prots; o++, p++) { - options[o].name = prot[p].description; - options[o].has_arg = required_argument; - options[o].flag = 0; - options[o].val = p + PROT_SHIFT; - } -} - diff --git a/common.h b/common.h index 27dd3d3..7fabd9f 100755 --- a/common.h +++ b/common.h @@ -1,3 +1,7 @@ +#ifndef __COMMON_H_ +#define __COMMON_H_ + + #define _GNU_SOURCE #include #include @@ -47,21 +51,6 @@ enum connection_state { ST_SHOVELING /* Connexion is established */ }; -typedef int T_PROTO_ID; /* Index into protocols[] array */ - -/* For each protocol we need: */ -struct proto { - int affected; /* are we actually using it? */ - char* description; /* a string that says what it is (for logging and command-line parsing) */ - char* service; /* service name to do libwrap checks */ - struct addrinfo saddr; /* where to switch that protocol */ - int (*probe)(const char*, int); /* function to probe that protocol */ -}; - -/* A table in common.c contains all the known protocols */ -extern struct proto protocols[]; -extern int num_known_protocols; - /* this is used to pass protocols through the command-line parameter parsing */ #define PROT_SHIFT 1000 /* protocol options will be 1000, 1001, etc */ @@ -91,23 +80,21 @@ struct connection { /* common.c */ void init_cnx(struct connection *cnx); -int connect_addr(struct addrinfo *addr, char* cnx_name); +int connect_addr(struct addrinfo *addr, const char* cnx_name); int fd2fd(struct queue *target, struct queue *from); char* sprintaddr(char* buf, size_t size, struct addrinfo *a); void resolve_name(struct addrinfo **out, char* fullname); -T_PROTO_ID probe_client_protocol(struct connection *cnx); +struct proto* probe_client_protocol(struct connection *cnx); void log_connection(struct connection *cnx); -int check_access_rights(int in_socket, char* service); +int check_access_rights(int in_socket, const char* service); void setup_signals(void); -void setup_syslog(char* bin_name); -void drop_privileges(char* user_name); -void write_pid_file(char* pidfile); -void printsettings(void); -void parse_cmdline(int argc, char* argv[]); +void setup_syslog(const char* bin_name); +void drop_privileges(const char* user_name); +void write_pid_file(const char* pidfile); void log_message(int type, char* msg, ...); void dump_connection(struct connection *cnx); +int resolve_split_name(struct addrinfo **out, const char* hostname, const char* port); -void append_protocols(struct option *options, int n_opts, struct proto *prot, int n_prots); int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list); int defer_write(struct queue *q, void* data, int data_size); @@ -117,10 +104,12 @@ extern int probing_timeout, verbose, inetd, foreground, numeric; extern struct sockaddr_storage addr_ssl, addr_ssh, addr_openvpn; extern struct addrinfo *addr_listen; extern const char* USAGE_STRING; -extern char* user_name, *pid_file; +extern const char* user_name, *pid_file, *rule_filename; extern const char* server_type; /* sslh-fork.c */ void start_shoveler(int); void main_loop(int *listen_sockets, int num_addr_listen); + +#endif diff --git a/example.cfg b/example.cfg new file mode 100644 index 0000000..c40d946 --- /dev/null +++ b/example.cfg @@ -0,0 +1,40 @@ +verbose: false; +foreground: true; +inetd: false; +numeric: false; +timeout: 2; +user: "nobody"; +pidfile: "/var/run/sslh.pid"; + + +# List of interfaces on which we should listen +listen: +( + { host: "thelonious"; port: "443"; } +# , { host: "thelonious"; port: "8080"; } +); + +# List of protocols +# +# Each protocol entry consists of: +# name: name of the protocol +# service: (optional) libwrap service name (see hosts_access(5)) +# host: host name to connect that protocol +# port: port number to connect that protocol +# probe: "builtin" or a list of regular expressions +# +# In case of timeout sslh will connect to the first +# protocol: this should be SSH. +# SSL should have a "always true" probe, and come last. +# sslh will try each probe in order they are declared, and +# connect to the first that matches. + +protocols: +( + { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; }, + { name: "openvpn"; host: "localhost"; port: "1194"; probe: [ "^\x00[\x0D-\xFF]$", "^\x00[\x0D-\xFF]\x38" ]; }, + { name: "xmpp"; host: "localhost"; port: "5222"; probe: [ "jabber" ]; }, + { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; }, + { name: "ssl"; host: "localhost"; port: "443"; probe: [ "" ]; } +); + diff --git a/probe.c b/probe.c new file mode 100644 index 0000000..714fd70 --- /dev/null +++ b/probe.c @@ -0,0 +1,216 @@ +/* +# probe.c: Code for probing protocols +# +# Copyright (C) 2007-2012 Yves Rutschle +# +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation; either +# version 2 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 General Public License for more +# details. +# +# The full text for the General Public License is here: +# http://www.gnu.org/licenses/gpl.html +*/ + +#include +#include "probe.h" + + + +static int is_ssh_protocol(const char *p, int len, struct proto*); +static int is_openvpn_protocol(const char *p, int len, struct proto*); +static int is_tinc_protocol(const char *p, int len, struct proto*); +static int is_xmpp_protocol(const char *p, int len, struct proto*); +static int is_http_protocol(const char *p, int len, struct proto*); +static int is_true(const char *p, int len, struct proto* proto) { return 1; } + +/* Table of protocols that have a built-in probe + */ +static struct proto builtins[] = { + /* description service saddr probe */ + { "ssh", "sshd", NULL, is_ssh_protocol}, + { "openvpn", NULL, NULL, is_openvpn_protocol }, + { "tinc", NULL, NULL, is_tinc_protocol }, + { "xmpp", NULL, NULL, is_xmpp_protocol }, + { "http", NULL, NULL, is_http_protocol }, + { "ssl", NULL, NULL, is_true } +}; + +static struct proto *protocols; + +struct proto* get_builtins(void) { + return builtins; +} + +int get_num_builtins(void) { + return ARRAY_SIZE(builtins); +} + +/* Returns the protocol to connect to in case of timeout; conventionaly this is + * the first protocol specified (but maybe we'll make it more explicit some + * day) + */ +struct proto* timeout_protocol(void) { + return protocols; +} + +/* returns the first protocol (caller can then follow the *next pointers) */ +struct proto* get_first_protocol(void) +{ + return protocols; +} + +void set_protocol_list(struct proto* prots) +{ + protocols = prots; +} + +/* Is the buffer the beginning of an SSH connection? */ +static int is_ssh_protocol(const char *p, int len, struct proto *proto) +{ + if (!strncmp(p, "SSH-", 4)) { + return 1; + } + return 0; +} + +/* Is the buffer the beginning of an OpenVPN connection? + * (code lifted from OpenVPN port-share option) + */ +static int is_openvpn_protocol (const char*p,int len, struct proto *proto) +{ +#define P_OPCODE_SHIFT 3 +#define P_CONTROL_HARD_RESET_CLIENT_V2 7 + if (len >= 3) + { + return p[0] == 0 + && p[1] >= 14 + && p[2] == (P_CONTROL_HARD_RESET_CLIENT_V2<= 2) + { + return p[0] == 0 && p[1] >= 14; + } + else + return 0; +} + +/* Is the buffer the beginning of a tinc connections? + * (protocol is undocumented, but starts with "0 " in 1.0.15) + * */ +static int is_tinc_protocol( const char *p, int len, struct proto *proto) +{ + return !strncmp(p, "0 ", 2); +} + +/* Is the buffer the beginning of a jabber (XMPP) connections? + * (Protocol is documented (http://tools.ietf.org/html/rfc6120) but for lazy + * clients, just checking first frame containing "jabber" in xml entity) + * */ +static int is_xmpp_protocol( const char *p, int len, struct proto *proto) +{ + return strstr(p, "jabber") ? 1 : 0; +} + +static int probe_http_method(const char *p, const char *opt) +{ + return !strcmp(p, opt); +} + +/* Is the buffer the beginning of an HTTP connection? */ +static int is_http_protocol(const char *p, int len, struct proto *proto) +{ + /* If it's got HTTP in the request (HTTP/1.1) then it's HTTP */ + if (strstr(p, "HTTP")) + return 1; + + /* Otherwise it could be HTTP/1.0 without version: check if it's got an + * HTTP method (RFC2616 5.1.1) */ + probe_http_method(p, "OPTIONS"); + probe_http_method(p, "GET"); + probe_http_method(p, "HEAD"); + probe_http_method(p, "POST"); + probe_http_method(p, "PUT"); + probe_http_method(p, "DELETE"); + probe_http_method(p, "TRACE"); + probe_http_method(p, "CONNECT"); + + return 0; +} + + +static int regex_probe(const char *p, int len, struct proto *proto) +{ + regex_t** probe_list = (regex_t**)(proto->data); + int i=0; + + while (probe_list[i]) { + if (!regexec(probe_list[i], p, 0, NULL, 0)) { + return 1; + } + i++; + } + return 0; +} + +/* + * Read the beginning of data coming from the client connection and check if + * it's a known protocol. Then leave the data on the defered + * write buffer of the connection and returns a pointer to the protocol + * structure + */ +struct proto* probe_client_protocol(struct connection *cnx) +{ + char buffer[BUFSIZ]; + struct proto *p; + int n; + + n = read(cnx->q[0].fd, buffer, sizeof(buffer)); + /* It's possible that read() returns an error, e.g. if the client + * disconnected between the previous call to select() and now. If that + * happens, we just connect to the default protocol so the caller of this + * function does not have to deal with a specific failure condition (the + * connection will just fail later normally). */ + if (n > 0) { + defer_write(&cnx->q[1], buffer, n); + + for (p = protocols; p; p = p->next) { + if (p->probe(buffer, n, p)) { + return p; + } + } + } + + /* If none worked, return the first one affected (that's completely + * arbitrary) */ + return protocols; +} + +/* Returns the probe for specified protocol: + * parameter is the description in builtins[], or "regex" + * */ +T_PROBE* get_probe(const char* description) { + int i; + + for (i = 0; i < ARRAY_SIZE(builtins); i++) { + if (!strcmp(builtins[i].description, description)) { + return builtins[i].probe; + } + } + /* Special case of "regex" probe (we don't want to set it in builtins + * because builtins is also used to build the command-line options and + * regexp is not legal on the command line)*/ + if (!strcmp(description, "regex")) + return regex_probe; + + return NULL; +} + + diff --git a/probe.h b/probe.h new file mode 100644 index 0000000..76073e6 --- /dev/null +++ b/probe.h @@ -0,0 +1,54 @@ +/* API for probe.c */ + +#ifndef __PROBE_H_ +#define __PROBE_H_ + +#include "common.h" + +struct proto; +typedef int T_PROBE(const char*, int, struct proto*); + +/* For each protocol we need: */ +struct proto { + const char* description; /* a string that says what it is (for logging and command-line parsing) */ + const char* service; /* service name to do libwrap checks */ + struct addrinfo *saddr; /* list of addresses to try and switch that protocol */ + + /* function to probe that protocol; parameters are buffer and length + * containing the data to probe, and a pointer to the protocol structure */ + T_PROBE* probe; + void* data; /* opaque pointer ; used to pass list of regex to regex probe */ + struct proto *next; /* pointer to next protocol in list, NULL if last */ +}; + +/* Returns a pointer to the array of builtin protocols */ +struct proto * get_builtins(void); + +/* Returns the number of builtin protocols */ +int get_num_builtins(void); + +/* Returns the probe for specified protocol */ +T_PROBE* get_probe(const char* description); + +/* Returns the head of the configured protocols */ +struct proto* get_first_protocol(void); + +/* Set the list of configured protocols */ +void set_protocol_list(struct proto*); + +/* probe_client_protocol + * + * Read the beginning of data coming from the client connection and check if + * it's a known protocol. Then leave the data on the defered + * write buffer of the connection and returns a pointer to the protocol + * structure + */ +struct proto* probe_client_protocol(struct connection *cnx); + +/* timeout_protocol + * + * Returns the protocol to connect to in case of timeout + */ +struct proto* timeout_protocol(void); + +#endif diff --git a/sslh-fork.c b/sslh-fork.c index e4bd69a..58ad985 100644 --- a/sslh-fork.c +++ b/sslh-fork.c @@ -1,7 +1,7 @@ /* - Reimplementation of sslh in C + sslh-fork: forking server -# Copyright (C) 2007-2011 Yves Rutschle +# Copyright (C) 2007-2012 Yves Rutschle # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public @@ -21,6 +21,7 @@ */ #include "common.h" +#include "probe.h" const char* server_type = "sslh-fork"; @@ -72,7 +73,7 @@ void start_shoveler(int in_socket) int res; int out_socket; struct connection cnx; - T_PROTO_ID prot; + struct proto *prot; init_cnx(&cnx); @@ -91,17 +92,17 @@ void start_shoveler(int in_socket) prot = probe_client_protocol(&cnx); } else { /* Timed out: it's necessarily SSH */ - prot = 0; + prot = timeout_protocol(); } - saddr = &protocols[prot].saddr; - if (protocols[prot].service && - check_access_rights(in_socket, protocols[prot].service)) { + saddr = prot->saddr; + if (prot->service && + check_access_rights(in_socket, prot->service)) { exit(0); } /* Connect the target socket */ - out_socket = connect_addr(saddr, protocols[prot].description); + out_socket = connect_addr(saddr, prot->description); CHECK_RES_DIE(out_socket, "connect"); cnx.q[1].fd = out_socket; diff --git a/sslh-main.c b/sslh-main.c index 3d4d5b0..126d151 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -1,7 +1,7 @@ /* -# main: processing of command line options and start the main loop. +# main: processing of config file, command line options and start the main loop. # -# Copyright (C) 2007-2011 Yves Rutschle +# Copyright (C) 2007-2012 Yves Rutschle # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public @@ -21,89 +21,369 @@ */ #define _GNU_SOURCE -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#ifdef LIBCONFIG +#include +#endif +#include #include "common.h" +#include "probe.h" const char* USAGE_STRING = "sslh " VERSION "\n" \ "usage:\n" \ -"\tsslh [-v] [-i] [-V] [-f] [-n]\n" +"\tsslh [-v] [-i] [-V] [-f] [-n] [-F ]\n" "\t[-t ] [-P ] -u -p [-p ...] \n" \ -"%s\n\n" \ +"%s\n\n" /* Dynamically built list of builtin protocols */ \ "-v: verbose\n" \ "-V: version\n" \ "-f: foreground\n" \ "-n: numeric output\n" \ +"-F: use configuration file\n" \ "-t: timeout before connecting to SSH.\n" \ "-p: address and port to listen on.\n Can be used several times to bind to several addresses.\n" \ "--[ssh,ssl,...]: where to connect connections from corresponding protocol.\n" \ -"-P: PID file. Default: /var/run/sslh.pid.\n" \ +"-F: specify a configuration file\n" \ +"-P: PID file.\n" \ "-i: Run as a inetd service.\n" \ ""; -void print_usage(void) +static struct option const_options[] = { + { "inetd", no_argument, &inetd, 1 }, + { "foreground", no_argument, &foreground, 1 }, + { "numeric", no_argument, &numeric, 1 }, + { "verbose", no_argument, &verbose, 1 }, + { "user", required_argument, 0, 'u' }, + { "config", required_argument, 0, 'F' }, + { "pidfile", required_argument, 0, 'P' }, + { "timeout", required_argument, 0, 't' }, + { "listen", required_argument, 0, 'p' }, + {} +}; +static struct option* all_options; +static struct proto* builtins; +static const char *optstr = "vt:T:p:VP:F:"; + + + +static void print_usage(void) { - int i; + struct proto *p; char *prots = ""; - for (i = 0; i < num_known_protocols; i++) - asprintf(&prots, "%s\t[--%s ]\n", prots, protocols[i].description); + for (p = get_first_protocol(); p; p = p->next) + asprintf(&prots, "%s\t[--%s ]\n", prots, p->description); fprintf(stderr, USAGE_STRING, prots); } -void parse_cmdline(int argc, char* argv[]) +static void printsettings(void) { - int c, affected = 0; - struct option const_options[] = { - { "inetd", no_argument, &inetd, 1 }, - { "foreground", no_argument, &foreground, 1 }, - { "verbose", no_argument, &verbose, 1 }, - { "numeric", no_argument, &numeric, 1 }, - { "user", required_argument, 0, 'u' }, - { "pidfile", required_argument, 0, 'P' }, - { "timeout", required_argument, 0, 't' }, - { "listen", required_argument, 0, 'p' }, - }; - struct option all_options[ARRAY_SIZE(const_options) + num_known_protocols + 1]; - struct addrinfo *addr, **a; + char buf[NI_MAXHOST]; + struct addrinfo *a; + struct proto *p; + + for (p = get_first_protocol(); p; p = p->next) { + fprintf(stderr, + "%s addr: %s. libwrap service: %s family %d %d\n", + p->description, + sprintaddr(buf, sizeof(buf), p->saddr), + p->service, + p->saddr->ai_family, + p->saddr->ai_addr->sa_family); + } + fprintf(stderr, "listening on:\n"); + for (a = addr_listen; a; a = a->ai_next) { + fprintf(stderr, "\t%s\n", sprintaddr(buf, sizeof(buf), a)); + } + fprintf(stderr, "timeout to ssh: %d\n", probing_timeout); +} - memset(all_options, 0, sizeof(all_options)); + +/* Extract configuration on addresses and ports on which to listen. + * out: newly allocated list of addrinfo to listen to + */ +#ifdef LIBCONFIG +static int config_listen(config_t *config, struct addrinfo **listen) +{ + config_setting_t *setting, *addr; + int len, i; + const char *hostname, *port; + + setting = config_lookup(config, "listen"); + if (setting) { + len = config_setting_length(setting); + for (i = 0; i < len; i++) { + addr = config_setting_get_elem(setting, i); + if (! (config_setting_lookup_string(addr, "host", &hostname) && + config_setting_lookup_string(addr, "port", &port))) { + fprintf(stderr, + "line %d:Incomplete specification (hostname and port required)\n", + config_setting_source_line(addr)); + return -1; + } + + resolve_split_name(listen, hostname, port); + + /* getaddrinfo returned a list of addresses corresponding to the + * specification; move the pointer to the end of that list before + * processing the next specification */ + for (; *listen; listen = &((*listen)->ai_next)); + } + } + + return 0; +} +#endif + + + +#ifdef LIBCONFIG +static void setup_regex_probe(struct proto *p, config_setting_t* probes) +{ + int num_probes, errsize, i, res; + char *err; + const char * expr; + regex_t** probe_list; + + num_probes = config_setting_length(probes); + if (!num_probes) { + fprintf(stderr, "%s: no probes specified\n", p->description); + exit(1); + } + + p->probe = get_probe("regex"); + probe_list = calloc(num_probes + 1, sizeof(*probe_list)); + p->data = (void*)probe_list; + + for (i = 0; i < num_probes; i++) { + probe_list[i] = malloc(sizeof(*(probe_list[i]))); + expr = config_setting_get_string_elem(probes, i); + res = regcomp(probe_list[i], expr, 0); + if (res) { + err = malloc(errsize = regerror(res, probe_list[i], NULL, 0)); + regerror(res, probe_list[i], err, errsize); + fprintf(stderr, "%s:%s\n", expr, err); + free(err); + exit(1); + } + } +} +#endif + +/* Extract configuration for protocols to connect to. + * out: newly-allocated list of protocols + */ +#ifdef LIBCONFIG +static int config_protocols(config_t *config, struct proto **prots) +{ + config_setting_t *setting, *prot, *probes; + const char *hostname, *port, *name; + int i, num_prots; + struct proto *p, *prev = NULL; + + setting = config_lookup(config, "protocols"); + if (setting) { + num_prots = config_setting_length(setting); + for (i = 0; i < num_prots; i++) { + p = calloc(1, sizeof(*p)); + if (i == 0) *prots = p; + if (prev) prev->next = p; + prev = p; + + prot = config_setting_get_elem(setting, i); + if ((config_setting_lookup_string(prot, "name", &name) && + config_setting_lookup_string(prot, "host", &hostname) && + config_setting_lookup_string(prot, "port", &port) + )) { + p->description = name; + config_setting_lookup_string(prot, "service", &(p->service)); + + resolve_split_name(&(p->saddr), hostname, port); + + + probes = config_setting_get_member(prot, "probe"); + if (config_setting_is_array(probes)) { + /* If 'probe' is an array, setup a regex probe using the + * array of strings as pattern */ + + setup_regex_probe(p, probes); + + } else { + /* if 'probe' is 'builtin', set the probe to the + * appropriate builtin protocol */ + if (!strcmp(config_setting_get_string(probes), "builtin")) { + p->probe = get_probe(name); + if (!p->probe) { + fprintf(stderr, "%s: no builtin probe for this protocol\n", name); + exit(1); + } + } else { + fprintf(stderr, "%s: illegal probe name\n", name); + exit(1); + } + } + } + } + } + + return 0; +} +#endif + +/* Parses a config file + * in: *filename + * out: *listen, a newly-allocated linked list of listen addrinfo + * *prots, a newly-allocated linked list of protocols + */ +#ifdef LIBCONFIG +static int config_parse(char *filename, struct addrinfo **listen, struct proto **prots) +{ + config_t config; + long int timeout; + + config_init(&config); + if (config_read_file(&config, filename) == CONFIG_FALSE) { + fprintf(stderr, "%s:%d:%s\n", + filename, + config_error_line(&config), + config_error_text(&config)); + exit(1); + } + + config_lookup_bool(&config, "verbose", &verbose); + config_lookup_bool(&config, "inetd", &inetd); + config_lookup_bool(&config, "foreground", &foreground); + config_lookup_bool(&config, "numeric", &numeric); + + if (config_lookup_int(&config, "timeout", &timeout) == CONFIG_TRUE) { + probing_timeout = timeout; + } + + config_lookup_string(&config, "user", &user_name); + config_lookup_string(&config, "pidfile", &pid_file); + + config_listen(&config, listen); + config_protocols(&config, prots); + + return 0; +} +#endif + +/* Adds protocols to the list of options, so command-line parsing uses the + * protocol definition array + * options: array of options to add to; must be big enough + * n_opts: number of options in *options before calling (i.e. where to append) + * prot: array of protocols + * n_prots: number of protocols in *prot + * */ +static void append_protocols(struct option *options, int n_opts, struct proto *prot , int n_prots) +{ + int o, p; + + for (o = n_opts, p = 0; p < n_prots; o++, p++) { + options[o].name = prot[p].description; + options[o].has_arg = required_argument; + options[o].flag = 0; + options[o].val = p + PROT_SHIFT; + } +} + +static void make_alloptions(void) +{ + builtins = get_builtins(); + + /* Create all_options, composed of const_options followed by one option per + * known protocol */ + all_options = calloc(ARRAY_SIZE(const_options) + get_num_builtins(), sizeof(struct option)); memcpy(all_options, const_options, sizeof(const_options)); - append_protocols(all_options, ARRAY_SIZE(const_options), protocols, num_known_protocols); + append_protocols(all_options, ARRAY_SIZE(const_options) - 1, builtins, get_num_builtins()); +} - while ((c = getopt_long_only(argc, argv, "t:T:p:VP:", all_options, NULL)) != -1) { +/* Performs a first scan of command line options to see if a configuration file + * is specified. If there is one, parse it now before all other options (so + * configuration file settings can be overridden from the command line). + * + * prots: newly-allocated list of configured protocols, if any. + */ +static void cmdline_config(int argc, char* argv[], struct proto** prots) +{ +#ifdef LIBCONFIG + int c, res; + char *config_filename; +#endif + + make_alloptions(); + +#ifdef LIBCONFIG + optind = 1; + opterr = 0; /* we're missing protocol options at this stage so don't output errors */ + while ((c = getopt_long_only(argc, argv, optstr, all_options, NULL)) != -1) { + if (c == 'F') { + config_filename = optarg; + /* find the end of the listen list */ + res = config_parse(config_filename, &addr_listen, prots); + if (res) + exit(4); + break; + } + } +#endif +} + + +/* Parse command-line options. prots points to a list of configured protocols, + * potentially non-allocated */ +static void parse_cmdline(int argc, char* argv[], struct proto* prots) +{ + int c; + struct addrinfo **a; + struct proto *p; + + optind = 1; + opterr = 1; +next_arg: + while ((c = getopt_long_only(argc, argv, optstr, all_options, NULL)) != -1) { if (c == 0) continue; if (c >= PROT_SHIFT) { - affected++; - protocols[c - PROT_SHIFT].affected = 1; - resolve_name(&addr, optarg); - protocols[c - PROT_SHIFT].saddr= *addr; + if (prots) + for (p = prots; p && p->next; p = p->next) { + /* override if protocol was already defined by config file + * (note it only overrides address and use builtin probe) */ + if (!strcmp(p->description, builtins[c-PROT_SHIFT].description)) { + resolve_name(&(p->saddr), optarg); + p->probe = builtins[c-PROT_SHIFT].probe; + goto next_arg; + } + } + /* At this stage, it's a new protocol: add it to the end of the + * list */ + if (!prots) { + /* No protocols yet -- create the list */ + p = prots = calloc(1, sizeof(*p)); + } else { + p->next = calloc(1, sizeof(*p)); + p = p->next; + } + memcpy(p, &builtins[c-PROT_SHIFT], sizeof(*p)); + resolve_name(&(p->saddr), optarg); continue; } switch (c) { + case 'F': + /* Legal option, but do nothing, it was already processed in + * cmdline_config() */ +#ifndef LIBCONFIG + fprintf(stderr, "Built without libconfig support: configuration file not available.\n"); + exit(1); +#endif + break; + case 't': - probing_timeout = atoi(optarg); + probing_timeout = atoi(optarg); break; case 'p': @@ -126,17 +406,23 @@ void parse_cmdline(int argc, char* argv[]) pid_file = optarg; break; + case 'v': + verbose++; + break; + default: print_usage(); exit(2); } } - if (!affected) { + if (!prots) { fprintf(stderr, "At least one target protocol must be specified.\n"); exit(2); } + set_protocol_list(prots); + if (!addr_listen) { fprintf(stderr, "No listening address specified; use at least one -p option\n"); exit(1); @@ -150,15 +436,16 @@ int main(int argc, char *argv[]) extern char *optarg; extern int optind; int res, num_addr_listen; + struct proto* protocols = NULL; int *listen_sockets; /* Init defaults */ pid_file = NULL; user_name = NULL; - foreground = 0; - parse_cmdline(argc, argv); + cmdline_config(argc, argv, &protocols); + parse_cmdline(argc, argv, protocols); if (inetd) { diff --git a/sslh-select.c b/sslh-select.c index 36d9f70..bdde468 100644 --- a/sslh-select.c +++ b/sslh-select.c @@ -1,5 +1,5 @@ /* - sslh: a SSL/SSH multiplexer + sslh-select: mono-processus server # Copyright (C) 2007-2010 Yves Rutschle # @@ -23,6 +23,7 @@ #define __LINUX__ #include "common.h" +#include "probe.h" const char* server_type = "sslh-select"; @@ -116,7 +117,7 @@ int accept_new_connection(int listen_socket, struct connection *cnx[], int* cnx_ /* Connect queue 1 of connection to SSL; returns new file descriptor */ int connect_queue(struct connection *cnx, struct addrinfo *addr, - char* cnx_name, + const char* cnx_name, fd_set *fds_r, fd_set *fds_w) { struct queue *q = &cnx->q[1]; @@ -196,7 +197,7 @@ void main_loop(int listen_sockets[], int num_addr_listen) struct timeval tv; int max_fd, in_socket, i, j, res; struct connection *cnx; - T_PROTO_ID prot; + struct proto *prot; int num_cnx; /* Number of connections in *cnx */ int num_probing = 0; /* Number of connections currently probing * We use this to know if we need to time out of @@ -237,7 +238,8 @@ void main_loop(int listen_sockets[], int num_addr_listen) for (i = 0; i < num_addr_listen; i++) { if (FD_ISSET(listen_sockets[i], &readfds)) { in_socket = accept_new_connection(listen_sockets[i], &cnx, &num_cnx); - num_probing++; + if (in_socket != -1) + num_probing++; if (in_socket > 0) { FD_SET(in_socket, &fds_r); @@ -293,20 +295,20 @@ void main_loop(int listen_sockets[], int num_addr_listen) /* If timed out it's SSH, otherwise the client sent * data so probe the protocol */ if ((cnx[i].probe_timeout < time(NULL))) { - prot = 0; + prot = timeout_protocol(); } else { prot = probe_client_protocol(&cnx[i]); } /* libwrap check if required for this protocol */ - if (protocols[prot].service && - check_access_rights(in_socket, protocols[prot].service)) { + if (prot->service && + check_access_rights(in_socket, prot->service)) { tidy_connection(&cnx[i], &fds_r, &fds_w); res = -1; } else { res = connect_queue(&cnx[i], - &protocols[prot].saddr, - protocols[prot].description, + prot->saddr, + prot->description, &fds_r, &fds_w); } diff --git a/sslh.pod b/sslh.pod index 07065b8..b2e6225 100644 --- a/sslh.pod +++ b/sslh.pod @@ -6,13 +6,14 @@ =head1 SYNOPSIS -sslh [ B<-t> I ] [B<-p> I [B<-p> I ...] [B<--ssl> I] [B<--ssh> I] [B<--openvpn> I] [B<--http> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n] +sslh [B<-F> I] [ B<-t> I ] [B<-p> I [B<-p> I ...] [B<--ssl> I] [B<--ssh> I] [B<--openvpn> I] [B<--http> I] [B<-u> I] [B<-P> I] [-v] [-i] [-V] [-f] [-n] =head1 DESCRIPTION -B accepts HTTP, HTTPS, SSH, OpenVPN, tinc and XMPP -connections on the same port. This makes it possible to -connect to any of these servers on port 443 (e.g. from +B accepts connections in HTTP, HTTPS, SSH, OpenVPN, +tinc, XMPP, or any other protocol that can be tested using a +regular expression, on the same port. This makes it possible +to connect to any of these servers on port 443 (e.g. from inside a corporate firewall, which almost never block port 443) while still serving HTTPS on that port. @@ -38,12 +39,11 @@ client, which is the case of OpenSSH and Putty), or the client sends its version first ("Bold" client, which is the case of Bitvise Tunnelier and ConnectBot). -B waits for some time for the incoming connection to -send data. If it stays quiet after the timeout period, it is -assumed to be a shy SSH client, and is connected to the SSH -server. Otherwise, B reads the first packet the client -provides, and connects it to the SSH server if it starts -with "SSH-", or connects it to the SSL server otherwise. +If the client stays quiet after the timeout period, B +will connect to the first protocol defined (in the +configuration file, or on the command line), so SSH should +be defined first in B configuration to accomodate for +shy SSH clients. =head2 Libwrap support @@ -56,13 +56,37 @@ B or B. For this reason, B can be compiled to check SSH accesses against SSH access lists as defined in F and F. +=head2 Configuration file + +A configuration file can be supplied to B. Command +line arguments override file settings. B uses +B to parse the configuration file, so the general +file format is indicated in +L. +Please refer to the example configuration file provided with +B for the specific format (Options have the same names +as on the command line, except for the list of listen ports +and the list of protocols). + +The configuration file makes it possible to specify +protocols using regular expressions: a list of regular +expressions is given as the I parameter, and if the +first packet received from the client matches any of these +expressions, B connects to that protocol. + +Alternatively, the I parameter can be set to +"builtin", to use the compiled probes which are much faster +than regular expressions. + + =head1 OPTIONS =over 4 =item B<-t> I, B<--timeout> I -Timeout before a connection is considered to be SSH. Default +Timeout before forwarding the connection to the first +configured protocol (which should usually be SSH). Default is 2s. =item B<-p> I, B<--listen> I @@ -125,7 +149,7 @@ Prints B version. Requires to run under the specified username. -=item B<-P> I, B<--pid-file> I +=item B<-P> I, B<--pidfile> I Specifies a file in which to write the PID of the main server. @@ -165,7 +189,7 @@ detailed explanation of the variables used by B. Last version available from L, and can be tracked -from L. +from L. =head1 AUTHOR diff --git a/t b/t index 6a95276..286dc1b 100755 --- a/t +++ b/t @@ -55,7 +55,9 @@ for my $binary (@binaries) { my $sslh_pid; if (!($sslh_pid = fork)) { my $user = (getpwuid $<)[0]; # Run under current username - exec "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile"; + my $cmd = "./$binary -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address --ssl $ssl_address -P $pidfile"; + warn "$cmd\n"; + exec $cmd; #exec "valgrind --leak-check=full ./sslh-select -v -f -u $user --listen localhost:$sslh_port --ssh $ssh_address -ssl $ssl_address -P $pidfile"; exit 0; } diff --git a/t_load b/t_load index d1bb683..9d145f9 100755 --- a/t_load +++ b/t_load @@ -27,7 +27,7 @@ my $NUM_CNX = 30; # you start 200 processes in under a second, things go wrong # and it's not sslh's fault (typically the echosrv won't be # forking fast enough). -my $start_time_delay = 0.5; +my $start_time_delay = 1; # If you test 4 protocols, you'll start $NUM_CNX * 4 clients # (e.g. 40), starting one every $start_time_delay seconds.