diff --git a/Makefile b/Makefile index 46bc9c6..b1cf9ce 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ USELIBCONFIG=1 # Use libconfig? (necessary to use configuration files) USELIBPCRE= # Use libpcre? (needed for regex on musl) USELIBWRAP?= # Use libwrap? USELIBCAP= # Use libcap? +USESYSTEMD= # Make use of systemd socket activation COV_TEST= # Perform test coverage? PREFIX?=/usr BINDIR?=$(PREFIX)/sbin @@ -50,6 +51,12 @@ ifneq ($(strip $(USELIBCAP)),) CPPFLAGS+=-DLIBCAP endif +ifneq ($(strip $(USESYSTEMD)),) + LIBS:=$(LIBS) -lsystemd + CPPFLAGS+=-DSYSTEMD +endif + + all: sslh $(MAN) echosrv .c.o: *.h @@ -68,6 +75,9 @@ sslh-select: version.h $(OBJS) sslh-select.o Makefile common.h $(CC) $(CFLAGS) $(LDFLAGS) -o sslh-select sslh-select.o $(OBJS) $(LIBS) #strip sslh-select +systemd-sslh-generator: systemd-sslh-generator.o + $(CC) $(CFLAGS) $(LDFLAGS) -o systemd-sslh-generator systemd-sslh-generator.o -lconfig + echosrv: $(OBJS) echosrv.o $(CC) $(CFLAGS) $(LDFLAGS) -o echosrv echosrv.o probe.o common.o tls.o $(LIBS) @@ -100,7 +110,7 @@ distclean: clean rm -f tags cscope.* clean: - rm -f sslh-fork sslh-select echosrv version.h $(MAN) *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info + rm -f sslh-fork sslh-select echosrv version.h $(MAN) systemd-sslh-generator *.o *.gcov *.gcno *.gcda *.png *.html *.css *.info tags: ctags --globals -T *.[ch] diff --git a/README.md b/README.md index a6c50de..c657e85 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ of the Makefile: file. You will need `libconfig` headers to compile (`libconfig8-dev` in Debian). +* `USESYSTEMD` compiles support for using systemd socket activation. + You will need `systemd` headers to compile (`systemd-devel` in Fedora). Binaries -------- @@ -303,6 +305,90 @@ explicit IP addresses (or names): This will not work: sslh --listen 192.168.0.1:443 --ssh 127.0.0.1:22 --ssl 127.0.0.1:4443 + +Transparent proxying means the target server sees the real +origin address, so it means if the client connects using +IPv6, the server must also support IPv6. It is easy to +support both IPv4 and IPv6 by configuring the server +accordingly, and setting `sslh` to connect to a name that +resolves to both IPv4 and IPv6, e.g.: + + sslh --transparent --listen :443 --ssh insideaddr:22 + + /etc/hosts: + 192.168.0.1 insideaddr + 201::::2 insideaddr + +Upon incoming IPv6 connection, `sslh` will first try to +connect to the IPv4 address (which will fail), then connect +to the IPv6 address. + +Systemd Socket Activation +------------------------- +If compiled with `USESYSTEMD` then it is possible to activate +the service on demand and avoid running any code as root. + +In this mode any listen configuration options are ignored and +the sockets are passed by systemd to the service. + +Example socket unit: + + [Unit] + Before=sslh.service + + [Socket] + ListenStream=1.2.3.4:443 + ListenStream=5.6.7.8:444 + ListenStream=9.10.11.12:445 + FreeBind=true + + [Install] + WantedBy=sockets.target + +Example service unit: + + [Unit] + PartOf=sslh.socket + + [Service] + ExecStart=/usr/sbin/sslh -v -f --ssh 127.0.0.1:22 --ssl 127.0.0.1:443 + KillMode=process + CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETGID CAP_SETUID + PrivateTmp=true + PrivateDevices=true + ProtectSystem=full + ProtectHome=true + User=sslh + + +With this setup only the socket needs to be enabled. The sslh service +will be started on demand and does not need to run as root to bind the +sockets as systemd has already bound and passed them over. If the sslh +service is started on its own without the sockets being passed by systemd +then it will look to use those defined on the command line or config +file as usual. Any number of ListenStreams can be defined in the socket +file and systemd will pass them all over to sslh to use as usual. + +To avoid inconsistency between starting via socket and starting directly +via the service Requires=sslh.socket can be added to the service unit to +mandate the use of the socket configuration. + +Rather than overwriting the entire socket file drop in values can be placed +in /etc/systemd/system/sslh.socket.d/.conf with additional ListenStream +values that will be merged. + +In addition to the above with manual .socket file configuration there is an +optional systemd generator which can be compiled - systemd-sslh-generator + +This parses the /etc/sslh.cfg (or /etc/sslh/sslh.cfg file if that exists +instead) configuration file and dynamically generates a socket file to use. + +This will also merge with any sslh.socket.d drop in configuration but will be +overriden by a /etc/systemd/system/sslh.socket file. + +To use the generator place it in /usr/lib/systemd/system-generators and then +call systemctl daemon-reload after any changes to /etc/sslh.cfg to generate +the new dynamic socket unit. Transparent proxying means the target server sees the real origin address, so it means if the client connects using diff --git a/common.c b/common.c index abf485c..171a89a 100644 --- a/common.c +++ b/common.c @@ -11,13 +11,19 @@ #include "common.h" #include "probe.h" -/* Added to make the code compilable under CYGWIN +/* Added to make the code compilable under CYGWIN * */ #ifndef SA_NOCLDWAIT #define SA_NOCLDWAIT 0 #endif -/* +/* Make use of systemd socket activation + * */ +#ifdef SYSTEMD +#include +#endif + +/* * 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 * parameters @@ -44,14 +50,35 @@ void check_res_dumpdie(int res, struct addrinfo *addr, char* syscall) char buf[NI_MAXHOST]; if (res == -1) { - fprintf(stderr, "%s:%s: %s\n", - sprintaddr(buf, sizeof(buf), addr), - syscall, + fprintf(stderr, "%s:%s: %s\n", + sprintaddr(buf, sizeof(buf), addr), + syscall, strerror(errno)); exit(1); } } +int get_fd_sockets(int *sockfd[]) +{ + int sd = 0; + +#ifdef SYSTEMD + sd = sd_listen_fds(0); + if (sd < 0) { + fprintf(stderr, "sd_listen_fds(): %s\n", strerror(-sd)); + exit(1); + } + if (sd > 0) { + *sockfd = malloc(sd * sizeof(*sockfd[0])); + for (int i = 0; i < sd; i++) { + (*sockfd)[i] = SD_LISTEN_FDS_START + i; + } + } +#endif + + return sd; +} + /* Starts listening sockets on specified addresses. * IN: addr[], num_addr * OUT: *sockfd[] pointer to newly-allocated array of file descriptors @@ -64,6 +91,13 @@ int start_listen_sockets(int *sockfd[], struct addrinfo *addr_list) struct addrinfo *addr; int i, res, one; int num_addr = 0; + int sd_socks = 0; + + sd_socks = get_fd_sockets(sockfd); + + if (sd_socks > 0) { + return sd_socks; + } for (addr = addr_list; addr; addr = addr->ai_next) num_addr++; @@ -147,7 +181,7 @@ int bind_peer(int fd, int fd_from) } /* Connect to first address that works and returns a file descriptor, or -1 if - * none work. + * none work. * If transparent proxying is on, use fd_from peer address on external address * of new file descriptor. */ int connect_addr(struct connection *cnx, int fd_from) @@ -168,8 +202,8 @@ int connect_addr(struct connection *cnx, int fd_from) /* When transparent, make sure both connections use the same address family */ if (transparent && a->ai_family != from.ai_addr->sa_family) continue; - if (verbose) - fprintf(stderr, "connecting to %s family %d len %d\n", + if (verbose) + fprintf(stderr, "connecting to %s family %d len %d\n", sprintaddr(buf, sizeof(buf), a), a->ai_addr->sa_family, a->ai_addrlen); @@ -185,7 +219,7 @@ int connect_addr(struct connection *cnx, int fd_from) } res = connect(fd, a->ai_addr, a->ai_addrlen); if (res == -1) { - log_message(LOG_ERR, "forward to %s failed:connect: %s\n", + log_message(LOG_ERR, "forward to %s failed:connect: %s\n", cnx->proto->description, strerror(errno)); close(fd); } else { @@ -203,10 +237,10 @@ int connect_addr(struct connection *cnx, int fd_from) } /* Store some data to write to the queue later */ -int defer_write(struct queue *q, void* data, int data_size) +int defer_write(struct queue *q, void* data, int data_size) { char *p; - if (verbose) + if (verbose) fprintf(stderr, "**** writing deferred on fd %d\n", q->fd); p = realloc(q->begin_deferred_data, q->deferred_data_size + data_size); @@ -270,7 +304,7 @@ void dump_connection(struct connection *cnx) } -/* +/* * moves data from one fd to other * * returns number of bytes copied if success @@ -338,16 +372,16 @@ char* sprintaddr(char* buf, size_t size, struct addrinfo *a) int res; res = getnameinfo(a->ai_addr, a->ai_addrlen, - host, sizeof(host), - serv, sizeof(serv), + host, sizeof(host), + serv, sizeof(serv), numeric ? NI_NUMERICHOST | NI_NUMERICSERV : 0 ); if (res) { log_message(LOG_ERR, "sprintaddr:getnameinfo: %s\n", gai_strerror(res)); /* Name resolution failed: do it numerically instead */ res = getnameinfo(a->ai_addr, a->ai_addrlen, - host, sizeof(host), - serv, sizeof(serv), + host, sizeof(host), + serv, sizeof(serv), NI_NUMERICHOST | NI_NUMERICSERV); /* should not fail but... */ if (res) { @@ -362,7 +396,7 @@ 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 +/* 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) @@ -450,7 +484,7 @@ void log_connection(struct connection *cnx) addr.ai_addrlen = sizeof(ss); res = getpeername(cnx->q[0].fd, addr.ai_addr, &addr.ai_addrlen); - if (res == -1) return; /* Can happen if connection drops before we get here. + if (res == -1) return; /* Can happen if connection drops before we get here. In that case, don't log anything (there is no connection) */ sprintaddr(peer, sizeof(peer), &addr); @@ -553,7 +587,7 @@ void setup_signals(void) } -/* Open syslog connection with appropriate banner; +/* Open syslog connection with appropriate banner; * banner is made up of basename(bin_name)+"[pid]" */ void setup_syslog(const char* bin_name) { char *name1, *name2; @@ -563,7 +597,7 @@ void setup_syslog(const char* bin_name) { res = asprintf(&name2, "%s[%d]", basename(name1), getpid()); CHECK_RES_DIE(res, "asprintf"); openlog(name2, LOG_CONS, LOG_AUTH); - free(name1); + free(name1); /* Don't free name2, as openlog(3) uses it (at least in glibc) */ log_message(LOG_INFO, "%s %s started\n", server_type, VERSION); @@ -635,7 +669,7 @@ void drop_privileges(const char* user_name) /* remove extraneous groups in case we belong to several extra groups that * may have unwanted rights. If non-root when calling setgroups(), it - * fails, which is fine because... we have no unwanted rights + * fails, which is fine because... we have no unwanted rights * (see POS36-C for security context) * */ setgroups(0, NULL); diff --git a/sslh-main.c b/sslh-main.c index 81f937b..8f9c515 100644 --- a/sslh-main.c +++ b/sslh-main.c @@ -564,10 +564,13 @@ next_arg: set_protocol_list(prots); +/* If compiling with systemd socket support no need to require listen address */ +#ifndef SYSTEMD if (!addr_listen && !inetd) { fprintf(stderr, "No listening address specified; use at least one -p option\n"); exit(1); } +#endif /* Did command-line override foreground setting? */ if (background) @@ -604,6 +607,13 @@ int main(int argc, char *argv[]) num_addr_listen = start_listen_sockets(&listen_sockets, addr_listen); +#ifdef SYSTEMD + if (num_addr_listen < 1) { + fprintf(stderr, "No listening sockets found, restart sockets or specify addresses in config\n"); + exit(1); + } +#endif + if (!foreground) { if (fork() > 0) exit(0); /* Detach */ diff --git a/systemd-sslh-generator.c b/systemd-sslh-generator.c new file mode 100644 index 0000000..5909a5b --- /dev/null +++ b/systemd-sslh-generator.c @@ -0,0 +1,153 @@ +#include +#include +#include +#include + + +static char* resolve_listen(const char *hostname, const char *port) { + +/* Need room in the strcat for \0 and : + * the format in the socket unit file is hostname:port */ + char *conn = (char*)malloc(strlen(hostname)+strlen(port)+2); + strcpy(conn, hostname); + strcat(conn, ":"); + strcat(conn, port); + + return conn; + +} + + +static int get_listen_from_conf(const char *filename, char **listen) { + + config_t config; + config_setting_t *setting, *addr; + const char *hostname, *port; + int len = 0; + +/* look up the listen stanzas in the config file so these + * can be used in the socket file generated */ + + config_init(&config); + if (config_read_file(&config, filename) == CONFIG_FALSE) { + /* we don't care if file is missing, skip it */ + if (config_error_line(&config) != 0) { + fprintf(stderr, "%s:%d:%s\n", + filename, + config_error_line(&config), + config_error_text(&config)); + return -1; + } + } else { + setting = config_lookup(&config, "listen"); + if (setting) { + len = config_setting_length(setting); + for (int 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; + } else { + + listen[i] = malloc(strlen(resolve_listen(hostname, port))); + strcpy(listen[i], resolve_listen(hostname, port)); + } + } + } + } + + return len; + +} + +static int write_socket_unit(FILE *socket, char **listen, int num_addr, const char *source) { + + fprintf(socket, + "# Automatically generated by systemd-sslh-generator\n\n" + "[Unit]\n" + "Before=sslh.service\n" + "SourcePath=%s\n" + "Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n" + "[Socket]\n" + "FreeBind=true\n", + source); + + for (int i = 0; i < num_addr; i++) { + fprintf(socket, "ListenStream=%s\n", listen[i]); + } + +return 0; +} + +static int gen_sslh_config(char *runtime_unit_dir) { + + char *sslh_conf; + int num_addr; + FILE *config; + char **listen; + FILE *runtime_conf_fd = stdout; + const char *unit_file; + +/* There are two default locations so check both with first given preference */ + sslh_conf = "/etc/sslh.cfg"; + + config = fopen(sslh_conf, "r"); + if (config == NULL) { + sslh_conf="/etc/sslh/sslh.cfg"; + config = fopen(sslh_conf, "r"); + if (config == NULL) { + return -1; + } + } + + fclose(config); + + + num_addr = get_listen_from_conf(sslh_conf, listen); + if (num_addr < 0) + return -1; + +/* If this is run by systemd directly write to the location told to + * otherwise write to standard out so that it's trivial to check what + * will be written */ + if (runtime_unit_dir != "") { + unit_file = "/sslh.socket"; + size_t uf_len = strlen(unit_file); + size_t runtime_len = strlen(runtime_unit_dir) + uf_len + 1; + char *runtime_conf = malloc(runtime_len); + strcpy(runtime_conf, runtime_unit_dir); + strcat(runtime_conf, unit_file); + runtime_conf_fd = fopen(runtime_conf, "w"); + } + + + return write_socket_unit(runtime_conf_fd, listen, num_addr, sslh_conf); + +} + +int main(int argc, char *argv[]){ + + int r = 0; + int k; + char *runtime_unit_dest = ""; + + if (argc > 1 && (argc != 4) ) { + printf("This program takes three or no arguments.\n"); + return -1; + } + + if (argc > 1) + runtime_unit_dest = argv[1]; + + k = gen_sslh_config(runtime_unit_dest); + if (k < 0) + r = k; + + return r < 0 ? -1 : 0; + +} + +