diff --git a/.gitignore b/.gitignore index 33c0e820..51e74a16 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ libsslh.a sslh-fork sslh-select sslh-ev +systemd-sslh-generator sslh.8.gz tags version.h diff --git a/scripts/systemd.sslh-select@.service b/scripts/systemd.sslh-select@.service new file mode 100644 index 00000000..a85c6e2e --- /dev/null +++ b/scripts/systemd.sslh-select@.service @@ -0,0 +1,27 @@ +[Unit] +Description=SSL/SSH multiplexer (select mode) for %I +After=network.target + +[Service] +EnvironmentFile=/etc/conf.d/sslh +ExecStart=/usr/sbin/sslh-select -F/etc/sslh/%I.cfg -f $DAEMON_OPTS +KillMode=process +#Hardening +PrivateTmp=true +CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +SecureBits=noroot-locked +ProtectSystem=strict +ProtectHome=true +ProtectKernelModules=true +ProtectKernelTunables=true +ProtectControlGroups=true +MountFlags=private +NoNewPrivileges=true +PrivateDevices=true +RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX +MemoryDenyWriteExecute=true +DynamicUser=true + +[Install] +WantedBy=multi-user.target diff --git a/scripts/systemd.sslh.service b/scripts/systemd.sslh@.service similarity index 82% rename from scripts/systemd.sslh.service rename to scripts/systemd.sslh@.service index a6a6bb9b..ad4561b5 100644 --- a/scripts/systemd.sslh.service +++ b/scripts/systemd.sslh@.service @@ -1,10 +1,10 @@ [Unit] -Description=SSL/SSH multiplexer +Description=SSL/SSH multiplexer (fork mode) for %I After=network.target [Service] EnvironmentFile=/etc/conf.d/sslh -ExecStart=/usr/sbin/sslh --foreground $DAEMON_OPTS +ExecStart=/usr/sbin/sslh -F/etc/sslh/%I.cfg -f $DAEMON_OPTS KillMode=process #Hardening PrivateTmp=true diff --git a/systemd-sslh-generator.c b/systemd-sslh-generator.c index 640e7cf0..bfddf84e 100644 --- a/systemd-sslh-generator.c +++ b/systemd-sslh-generator.c @@ -1,39 +1,72 @@ #include +#include #include +#include #include #include +#include +#include +#include #include "common.h" -#define print_message(sink, format, file, line) fprintf(stderr, format, file, line) +#define print_message(sink, format, file, line) fprintf(stderr, format, file, line) -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 = malloc(strlen(hostname)+strlen(port)+2); - CHECK_ALLOC(conn, "malloc"); +typedef struct FileList FileList; + +struct FileList { + char *name; + struct FileList *prev; +}; + +static void free_file_list(FileList *fl) { + while (fl != NULL) { + FileList *prev = fl->prev; + free(fl->name); + free(fl); + fl = prev; + } +} + +static FILE *err_log; +static bool systemd_invoked = false; +static const char + *socket_ext = ".socket", + *dropin_ext = ".conf", + *service_ext = ".service.d/", + *fork_unit_file = "/sslh@", + *select_unit_file = "/sslh-select@", + *fork_unit = "# Automatically generated by systemd-sslh-generator\n\n" + "[Unit]\n" + "Requires=sslh@%s.socket\n" + "Conflicts=sslh-select@%s.service\n" + "PartOf=sslh@%s.socket\n", + *select_unit = "# Automatically generated by systemd-sslh-generator\n\n" + "[Unit]\n" + "Requires=sslh@%s.socket\n" + "Conflicts=sslh@%s.service\n" + "PartOf=sslh@%s.socket\n"; + +static char *resolve_listen(const char *hostname, const char *port) { + char *conn = malloc(strlen(hostname) + strlen(port) + 2); + CHECK_ALLOC(conn, "malloc") 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", + fprintf(err_log, + "systemd-sslh-generator: %s%d%s\n", filename, config_error_line(&config), config_error_text(&config)); @@ -42,16 +75,14 @@ static int get_listen_from_conf(const char *filename, char **listen[]) { } else { setting = config_lookup(&config, "listen"); if (setting) { - int i; len = config_setting_length(setting); *listen = malloc(len * sizeof(**listen)); - CHECK_ALLOC(*listen, "malloc"); - for (i = 0; i < len; i++) { + CHECK_ALLOC(*listen, "malloc") + 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", + if (!(config_setting_lookup_string(addr, "host", &hostname) && config_setting_lookup_string(addr, "port", &port))) { + fprintf(err_log, + "systemd-sslh-generator: line %d:Incomplete specification (hostname and port required)\n", config_setting_source_line(addr)); return -1; } else { @@ -65,95 +96,231 @@ static int get_listen_from_conf(const char *filename, char **listen[]) { } } } - return len; - } -static int write_socket_unit(FILE *socket, char *listen[], int num_addr, const char *source) { - int i; - +static void write_socket_unit(FILE *socket, char *listen[], int num_addr, const char *cfg, const char *source) { fprintf(socket, "# Automatically generated by systemd-sslh-generator\n\n" "[Unit]\n" - "Before=sslh.service\n" + "Before=sslh@%s.service\n" "SourcePath=%s\n" + "PartOf=sslh@%s.service\n" "Documentation=man:sslh(8) man:systemd-sslh-generator(8)\n\n" "[Socket]\n" "FreeBind=true\n", - source); - - for (i = 0; i < num_addr; i++) { + cfg, + source, + cfg); + for (int i = 0; i < num_addr; i++) { fprintf(socket, "ListenStream=%s\n", listen[i]); } + fprintf(socket, + "\n[Install]\n" + "WantedBy=sockets.target\n"); +} + +static int write_unit_dropin(const char *runtime_unit_dir, const bool is_fork, const char *cfg) { + const char + *unit_file = is_fork ? fork_unit_file : select_unit_file, + *unit_string = is_fork ? fork_unit : select_unit; + FILE *dropin_fd = stdout; + if (systemd_invoked) { + //Systemd drop-in configuration for the base select service sslh@%I.service + const size_t runtime_len = strlen(runtime_unit_dir); + size_t len = strlen(unit_file) + strlen(cfg) + strlen(service_ext) + strlen(dropin_ext); + char dropin_dir[runtime_len + len + 1]; + strcpy(dropin_dir, runtime_unit_dir); + strcat(dropin_dir, unit_file); + strcat(dropin_dir, cfg); + strcat(dropin_dir, service_ext); + if (mkdir(dropin_dir, S_IRWXU | S_IRWXG | S_IROTH)) { + fprintf(err_log, + "systemd-sslh-generator: Could not create directory '%s' to generate drop-in configuration: %s\n", + dropin_dir, + strerror(errno)); + return errno; + } + + len = len + strlen(cfg) + strlen(dropin_ext); + char dropin_path[len + 1]; + strcpy(dropin_path, dropin_dir); + strcat(dropin_path, cfg); + strcat(dropin_path, dropin_ext); + dropin_fd = fopen(dropin_path, "w"); + if (!dropin_fd) { + fprintf(err_log, + "systemd-sslh-generator: Could not open '%s' to generate drop-in configuration: %s\n", + dropin_path, + strerror(errno)); + return errno; + } + } + fprintf(dropin_fd, unit_string, cfg, cfg, cfg); + if (systemd_invoked) { + fclose(dropin_fd); + } return 0; } static int gen_sslh_config(char *runtime_unit_dir) { - char *sslh_conf; - int num_addr; - FILE *config; + int status = 0; + const char *config_dir = "/etc/sslh/"; 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"; + DIR *d = opendir(config_dir); + FileList *fa = NULL; - config = fopen(sslh_conf, "r"); - if (config == NULL) { - sslh_conf="/etc/sslh/sslh.cfg"; - config = fopen(sslh_conf, "r"); - if (config == NULL) { - return -1; + if (d) { + struct dirent *dir; + while ((dir = readdir(d)) != NULL) { + if ((strcmp(dir->d_name, ".") == 0) || (strcmp(dir->d_name, "..") == 0)) { + continue; + } + FileList *lfa = malloc(sizeof(FileList)); + CHECK_ALLOC(lfa, "malloc") + lfa->name = malloc(strlen(dir->d_name) + 1); + CHECK_ALLOC(lfa->name, "malloc") + strcpy(lfa->name, dir->d_name); + lfa->prev = NULL; + if (fa) { + lfa->prev = fa; + } + fa = lfa; } + closedir(d); + } else { + fprintf(err_log, + "systemd-sslh-generator: Configuration directory '/etc/sslh/' does not exist! No units generated.\n"); + //Config directory /etc/sslh/ does not exist + return 0; } - - 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 && *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); - CHECK_ALLOC(runtime_conf, "malloc"); - strcpy(runtime_conf, runtime_unit_dir); - strcat(runtime_conf, unit_file); - runtime_conf_fd = fopen(runtime_conf, "w"); - free(runtime_conf); + if (!fa) { + fprintf(err_log, + "systemd-sslh-generator: Configuration directory '/etc/sslh/' is empty! No units generated.\n"); + //No configuration files in /etc/sslh/ + return 0; } + FileList *fa_ref = fa; +// size_t num_listen_addresses = 0; +// char **listen_addresses = NULL; + //Process all config files + do { + char *end = strstr(fa->name, ".cfg"); + if (!end) { + continue; + } + //Current sslh config name + const size_t end_count = end - fa->name; + char config_name[end_count + 1]; + memcpy(config_name, fa->name, end_count); + config_name[end_count] = '\0'; - - return write_socket_unit(runtime_conf_fd, listen, num_addr, sslh_conf); + //Full path to current sslh config + char full_path[strlen(config_dir) + strlen(fa->name) + 1]; + strcpy(full_path, config_dir); + strcat(full_path, fa->name); + FILE *config = fopen(full_path, "r"); + if (!config) { + fprintf(err_log, + "systemd-sslh-generator: Could not open config file '%s': %s\n", + full_path, + strerror(errno)); + return errno; + } else { + fclose(config); + int num_addr = get_listen_from_conf(full_path, &listen); + if (num_addr <= 0) { + fprintf(err_log, + "systemd-sslh-generator: sslh config '%s' contains no valid listen configurations!\n", + fa->name); + status |= -1; + continue; + } + FILE *socket_fd = stdout; + if (systemd_invoked) { + //Systemd socket for the current sslh config + const size_t len = strlen(runtime_unit_dir) + strlen(fork_unit_file) + strlen(config_name) + strlen(socket_ext); + char socket_path[len + 1]; + strcpy(socket_path, runtime_unit_dir); + strcat(socket_path, fork_unit_file); + strcat(socket_path, config_name); + strcat(socket_path, socket_ext); + socket_fd = fopen(socket_path, "w"); + if (!socket_fd) { + fprintf(err_log, + "systemd-sslh-generator: Could not open '%s' to generate socket configuration: %s\n", + socket_path, + strerror(errno)); + status |= errno; + continue; + } + } + //Write socket unit + write_socket_unit(socket_fd, listen, num_addr, config_name, full_path); + //Write forking drop-in config + status |= write_unit_dropin(runtime_unit_dir, false, config_name); + //Write select drop-in config + status |= write_unit_dropin(runtime_unit_dir, true, config_name); + if (systemd_invoked) { + fclose(socket_fd); + } +// if (listen_addresses) { +// //Check for overlapping addresses +// for (size_t i = 0; i < num_listen_addresses; i++) { +// for (size_t j = 0; j < num_addr; j++) { +// if (strcmp(*(listen_addresses + i), *(listen + j)) == 0) { +// fprintf(err_log, "systemd-sslh-generator: Overlapping listen addresses across sslh configurations!"); +// return -1; +// } +// } +// } +// } +// char *listen_addresses_copy[num_listen_addresses + num_addr]; +// if (listen_addresses) { +// //Copy previous configurations' listen addresses into temp +// memcpy(listen_addresses_copy, listen_addresses, num_listen_addresses); +// //Free global list of listen addresses +// free(listen_addresses); +// } +// //Copy listen addresses from current configuration into temp +// memcpy(listen_addresses_copy + num_listen_addresses, listen, num_addr); +// num_listen_addresses += num_addr; +// listen_addresses = malloc(num_listen_addresses * sizeof(char *)); +// CHECK_ALLOC(listen_addresses, "malloc") +// //Append all addresses to global list +// memcpy(listen_addresses, listen_addresses_copy, num_listen_addresses); + //Free all allocated listen strings + for (size_t i = 0; i < num_addr; i++) { + free(*(listen + i)); + } + } + } while((fa = fa->prev)); + free_file_list(fa_ref); + return status; } - -int main(int argc, char *argv[]){ - int r = 0; - int k; - char *runtime_unit_dest = ""; - - if (argc > 1 && (argc != 4) ) { +int main(int argc, char *argv[]) { + if (argc == 1 || argc == 4) { + systemd_invoked = argc == 4; + if (systemd_invoked) { + err_log = fopen("/dev/kmsg", "w"); + if (!err_log) { + return -1; + } + } else { + err_log = stderr; + } + const int r = gen_sslh_config(systemd_invoked ? argv[1] : ""); + if (systemd_invoked) { + fclose(err_log); + } + if (!r) { + fprintf(err_log, "systemd-sslh-generator: Successfully generated all targets.\n"); + } + return r < 0 ? -1 : 0; + } else { 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; } - -