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.