diff options
Diffstat (limited to 'src/master/master-settings.c')
-rw-r--r-- | src/master/master-settings.c | 806 |
1 files changed, 806 insertions, 0 deletions
diff --git a/src/master/master-settings.c b/src/master/master-settings.c new file mode 100644 index 0000000..7cfaa35 --- /dev/null +++ b/src/master/master-settings.c @@ -0,0 +1,806 @@ +/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */ + +#include "common.h" +#include "array.h" +#include "env-util.h" +#include "istream.h" +#include "net.h" +#include "str.h" +#include "ipwd.h" +#include "mkdir-parents.h" +#include "safe-mkdir.h" +#include "restrict-process-size.h" +#include "settings-parser.h" +#include "master-settings.h" + +#include <stddef.h> +#include <dirent.h> +#include <unistd.h> +#include <sys/stat.h> +#include <sys/wait.h> + +static bool master_settings_verify(void *_set, pool_t pool, + const char **error_r); + +extern const struct setting_parser_info service_setting_parser_info; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct file_listener_settings) + +static const struct setting_define file_listener_setting_defines[] = { + DEF(STR, path), + DEF(UINT_OCT, mode), + DEF(STR, user), + DEF(STR, group), + + SETTING_DEFINE_LIST_END +}; + +static const struct file_listener_settings file_listener_default_settings = { + .path = "", + .mode = 0600, + .user = "", + .group = "", +}; + +static const struct setting_parser_info file_listener_setting_parser_info = { + .defines = file_listener_setting_defines, + .defaults = &file_listener_default_settings, + + .type_offset = offsetof(struct file_listener_settings, path), + .struct_size = sizeof(struct file_listener_settings), + + .parent_offset = SIZE_MAX, + .parent = &service_setting_parser_info +}; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct inet_listener_settings) + +static const struct setting_define inet_listener_setting_defines[] = { + DEF(STR, name), + DEF(STR, address), + DEF(IN_PORT, port), + DEF(BOOL, ssl), + DEF(BOOL, reuse_port), + DEF(BOOL, haproxy), + + SETTING_DEFINE_LIST_END +}; + +static const struct inet_listener_settings inet_listener_default_settings = { + .name = "", + .address = "", + .port = 0, + .ssl = FALSE, + .reuse_port = FALSE, + .haproxy = FALSE +}; + +static const struct setting_parser_info inet_listener_setting_parser_info = { + .defines = inet_listener_setting_defines, + .defaults = &inet_listener_default_settings, + + .type_offset = offsetof(struct inet_listener_settings, name), + .struct_size = sizeof(struct inet_listener_settings), + + .parent_offset = SIZE_MAX, + .parent = &service_setting_parser_info +}; + +#undef DEF +#undef DEFLIST_UNIQUE +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct service_settings) +#define DEFLIST_UNIQUE(field, name, defines) \ + { .type = SET_DEFLIST_UNIQUE, .key = name, \ + .offset = offsetof(struct service_settings, field), \ + .list_info = defines } + +static const struct setting_define service_setting_defines[] = { + DEF(STR, name), + DEF(STR, protocol), + DEF(STR, type), + DEF(STR, executable), + DEF(STR, user), + DEF(STR, group), + DEF(STR, privileged_group), + DEF(STR, extra_groups), + DEF(STR, chroot), + + DEF(BOOL, drop_priv_before_exec), + + DEF(UINT, process_min_avail), + DEF(UINT, process_limit), + DEF(UINT, client_limit), + DEF(UINT, service_count), + DEF(TIME, idle_kill), + DEF(SIZE, vsz_limit), + + DEFLIST_UNIQUE(unix_listeners, "unix_listener", + &file_listener_setting_parser_info), + DEFLIST_UNIQUE(fifo_listeners, "fifo_listener", + &file_listener_setting_parser_info), + DEFLIST_UNIQUE(inet_listeners, "inet_listener", + &inet_listener_setting_parser_info), + + SETTING_DEFINE_LIST_END +}; + +static const struct service_settings service_default_settings = { + .name = "", + .protocol = "", + .type = "", + .executable = "", + .user = "", + .group = "", + .privileged_group = "", + .extra_groups = "", + .chroot = "", + + .drop_priv_before_exec = FALSE, + + .process_min_avail = 0, + .process_limit = 0, + .client_limit = 0, + .service_count = 0, + .idle_kill = 0, + .vsz_limit = UOFF_T_MAX, + + .unix_listeners = ARRAY_INIT, + .fifo_listeners = ARRAY_INIT, + .inet_listeners = ARRAY_INIT +}; + +const struct setting_parser_info service_setting_parser_info = { + .defines = service_setting_defines, + .defaults = &service_default_settings, + + .type_offset = offsetof(struct service_settings, name), + .struct_size = sizeof(struct service_settings), + + .parent_offset = offsetof(struct service_settings, master_set), + .parent = &master_setting_parser_info +}; + +#undef DEF +#undef DEFLIST_UNIQUE +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type(#name, name, struct master_settings) +#define DEFLIST_UNIQUE(field, name, defines) \ + { .type = SET_DEFLIST_UNIQUE, .key = name, \ + .offset = offsetof(struct master_settings, field), \ + .list_info = defines } + +static const struct setting_define master_setting_defines[] = { + DEF(STR, base_dir), + DEF(STR, state_dir), + DEF(STR, libexec_dir), + DEF(STR, instance_name), + DEF(STR, protocols), + DEF(STR, listen), + DEF(ENUM, ssl), + DEF(STR, default_internal_user), + DEF(STR, default_internal_group), + DEF(STR, default_login_user), + DEF(UINT, default_process_limit), + DEF(UINT, default_client_limit), + DEF(TIME, default_idle_kill), + DEF(SIZE, default_vsz_limit), + + DEF(BOOL, version_ignore), + + DEF(UINT, first_valid_uid), + DEF(UINT, last_valid_uid), + DEF(UINT, first_valid_gid), + DEF(UINT, last_valid_gid), + + DEFLIST_UNIQUE(services, "service", &service_setting_parser_info), + + SETTING_DEFINE_LIST_END +}; + +static const struct master_settings master_default_settings = { + .base_dir = PKG_RUNDIR, + .state_dir = PKG_STATEDIR, + .libexec_dir = PKG_LIBEXECDIR, + .instance_name = PACKAGE, + .protocols = "imap pop3 lmtp", + .listen = "*, ::", + .ssl = "yes:no:required", + .default_internal_user = "dovecot", + .default_internal_group = "dovecot", + .default_login_user = "dovenull", + .default_process_limit = 100, + .default_client_limit = 1000, + .default_idle_kill = 60, + .default_vsz_limit = 256*1024*1024, + + .version_ignore = FALSE, + + .first_valid_uid = 500, + .last_valid_uid = 0, + .first_valid_gid = 1, + .last_valid_gid = 0, + +#ifndef CONFIG_BINARY + .services = ARRAY_INIT +#else + .services = { { &config_all_services_buf, + sizeof(struct service_settings *) } }, +#endif +}; + +const struct setting_parser_info master_setting_parser_info = { + .module_name = "master", + .defines = master_setting_defines, + .defaults = &master_default_settings, + + .type_offset = SIZE_MAX, + .struct_size = sizeof(struct master_settings), + + .parent_offset = SIZE_MAX, + + .check_func = master_settings_verify +}; + +/* <settings checks> */ +static void +expand_user(const char **user, enum service_user_default *default_r, + const struct master_settings *set) +{ + /* $variable expansion is typically done by doveconf, but these + variables can come from built-in settings, so we need to expand + them here */ + if (strcmp(*user, "$default_internal_user") == 0) { + *user = set->default_internal_user; + *default_r = SERVICE_USER_DEFAULT_INTERNAL; + } else if (strcmp(*user, "$default_login_user") == 0) { + *user = set->default_login_user; + *default_r = SERVICE_USER_DEFAULT_LOGIN; + } else { + *default_r = SERVICE_USER_DEFAULT_NONE; + } +} + +static void +expand_group(const char **group, const struct master_settings *set) +{ + /* $variable expansion is typically done by doveconf, but these + variables can come from built-in settings, so we need to expand + them here */ + if (strcmp(*group, "$default_internal_group") == 0) + *group = set->default_internal_group; +} + +static bool +fix_file_listener_paths(ARRAY_TYPE(file_listener_settings) *l, + pool_t pool, const struct master_settings *master_set, + ARRAY_TYPE(const_string) *all_listeners, + const char **error_r) +{ + struct file_listener_settings *set; + size_t base_dir_len = strlen(master_set->base_dir); + enum service_user_default user_default; + + if (!array_is_created(l)) + return TRUE; + + array_foreach_elem(l, set) { + if (set->path[0] == '\0') { + *error_r = "path must not be empty"; + return FALSE; + } + + expand_user(&set->user, &user_default, master_set); + expand_group(&set->group, master_set); + if (*set->path != '/') { + set->path = p_strconcat(pool, master_set->base_dir, "/", + set->path, NULL); + } else if (strncmp(set->path, master_set->base_dir, + base_dir_len) == 0 && + set->path[base_dir_len] == '/') { + i_warning("You should remove base_dir prefix from " + "unix_listener: %s", set->path); + } + if (set->mode != 0) + array_push_back(all_listeners, &set->path); + } + return TRUE; +} + +static void add_inet_listeners(ARRAY_TYPE(inet_listener_settings) *l, + ARRAY_TYPE(const_string) *all_listeners) +{ + struct inet_listener_settings *set; + const char *str; + + if (!array_is_created(l)) + return; + + array_foreach_elem(l, set) { + if (set->port != 0) { + str = t_strdup_printf("%u:%s", set->port, set->address); + array_push_back(all_listeners, &str); + } + } +} + +static bool master_settings_parse_type(struct service_settings *set, + const char **error_r) +{ + if (*set->type == '\0') + set->parsed_type = SERVICE_TYPE_UNKNOWN; + else if (strcmp(set->type, "log") == 0) + set->parsed_type = SERVICE_TYPE_LOG; + else if (strcmp(set->type, "config") == 0) + set->parsed_type = SERVICE_TYPE_CONFIG; + else if (strcmp(set->type, "anvil") == 0) + set->parsed_type = SERVICE_TYPE_ANVIL; + else if (strcmp(set->type, "login") == 0) + set->parsed_type = SERVICE_TYPE_LOGIN; + else if (strcmp(set->type, "startup") == 0) + set->parsed_type = SERVICE_TYPE_STARTUP; + else if (strcmp(set->type, "worker") == 0) + set->parsed_type = SERVICE_TYPE_WORKER; + else { + *error_r = t_strconcat("Unknown service type: ", + set->type, NULL); + return FALSE; + } + return TRUE; +} + +static void service_set_login_dump_core(struct service_settings *set) +{ + const char *p; + + if (set->parsed_type != SERVICE_TYPE_LOGIN) + return; + + p = strstr(set->executable, " -D"); + if (p != NULL && (p[3] == '\0' || p[3] == ' ')) + set->login_dump_core = TRUE; +} + +static bool +services_have_protocol(struct master_settings *set, const char *name) +{ + struct service_settings *service; + + array_foreach_elem(&set->services, service) { + if (strcmp(service->protocol, name) == 0) + return TRUE; + } + return FALSE; +} + +#ifdef CONFIG_BINARY +static const struct service_settings * +master_default_settings_get_service(const char *name) +{ + extern struct master_settings master_default_settings; + struct service_settings *set; + + array_foreach_elem(&master_default_settings.services, set) { + if (strcmp(set->name, name) == 0) + return set; + } + return NULL; +} +#endif + +static unsigned int +service_get_client_limit(struct master_settings *set, const char *name) +{ + struct service_settings *service; + + array_foreach_elem(&set->services, service) { + if (strcmp(service->name, name) == 0) { + if (service->client_limit != 0) + return service->client_limit; + else + return set->default_client_limit; + } + } + return set->default_client_limit; +} + +static bool +master_settings_verify(void *_set, pool_t pool, const char **error_r) +{ + static bool warned_auth = FALSE, warned_anvil = FALSE; + struct master_settings *set = _set; + struct service_settings *const *services; + const char *const *strings; + ARRAY_TYPE(const_string) all_listeners; + struct passwd pw; + unsigned int i, j, count, client_limit, process_limit; + unsigned int max_auth_client_processes, max_anvil_client_processes; + string_t *max_auth_client_processes_reason = t_str_new(64); + string_t *max_anvil_client_processes_reason = t_str_new(64); + size_t len; +#ifdef CONFIG_BINARY + const struct service_settings *default_service; +#else + rlim_t fd_limit; + const char *max_client_limit_source = "default_client_limit"; + unsigned int max_client_limit = set->default_client_limit; +#endif + + if (*set->listen == '\0') { + *error_r = "listen can't be set empty"; + return FALSE; + } + + len = strlen(set->base_dir); + if (len > 0 && set->base_dir[len-1] == '/') { + /* drop trailing '/' */ + set->base_dir = p_strndup(pool, set->base_dir, len - 1); + } + + if (set->last_valid_uid != 0 && + set->first_valid_uid > set->last_valid_uid) { + *error_r = "first_valid_uid can't be larger than last_valid_uid"; + return FALSE; + } + if (set->last_valid_gid != 0 && + set->first_valid_gid > set->last_valid_gid) { + *error_r = "first_valid_gid can't be larger than last_valid_gid"; + return FALSE; + } + + if (i_getpwnam(set->default_login_user, &pw) == 0) { + *error_r = t_strdup_printf("default_login_user doesn't exist: %s", + set->default_login_user); + return FALSE; + } + if (i_getpwnam(set->default_internal_user, &pw) == 0) { + *error_r = t_strdup_printf("default_internal_user doesn't exist: %s", + set->default_internal_user); + return FALSE; + } + + /* check that we have at least one service. the actual service + structure validity is checked later while creating them. */ + if (!array_is_created(&set->services) || + array_count(&set->services) == 0) { + *error_r = "No services defined"; + return FALSE; + } + services = array_get(&set->services, &count); + for (i = 0; i < count; i++) { + struct service_settings *service = services[i]; + + if (*service->name == '\0') { + *error_r = t_strdup_printf( + "Service #%d is missing name", i); + return FALSE; + } + if (!master_settings_parse_type(service, error_r)) + return FALSE; + for (j = 0; j < i; j++) { + if (strcmp(service->name, services[j]->name) == 0) { + *error_r = t_strdup_printf( + "Duplicate service name: %s", + service->name); + return FALSE; + } + } + expand_user(&service->user, &service->user_default, set); + expand_group(&service->extra_groups, set); + service_set_login_dump_core(service); + } + set->protocols_split = p_strsplit_spaces(pool, set->protocols, " "); + if (set->protocols_split[0] != NULL && + strcmp(set->protocols_split[0], "none") == 0 && + set->protocols_split[1] == NULL) + set->protocols_split[0] = NULL; + + for (i = 0; set->protocols_split[i] != NULL; i++) { + if (!services_have_protocol(set, set->protocols_split[i])) { + *error_r = t_strdup_printf("protocols: " + "Unknown protocol: %s", + set->protocols_split[i]); + return FALSE; + } + } + t_array_init(&all_listeners, 64); + max_auth_client_processes = 0; + max_anvil_client_processes = 2; /* blocking, nonblocking pipes */ + for (i = 0; i < count; i++) { + struct service_settings *service = services[i]; + + if (*service->protocol != '\0' && + !str_array_find((const char **)set->protocols_split, + service->protocol)) { + /* protocol not enabled, ignore its settings */ + continue; + } + + if (*service->executable != '/' && + *service->executable != '\0') { + service->executable = + p_strconcat(pool, set->libexec_dir, "/", + service->executable, NULL); + } + if (*service->chroot != '/' && *service->chroot != '\0') { + service->chroot = + p_strconcat(pool, set->base_dir, "/", + service->chroot, NULL); + } + if (service->drop_priv_before_exec && + *service->chroot != '\0') { + *error_r = t_strdup_printf("service(%s): " + "drop_priv_before_exec=yes can't be " + "used with chroot", service->name); + return FALSE; + } + process_limit = service->process_limit; + if (process_limit == 0) + process_limit = set->default_process_limit; + if (service->process_min_avail > process_limit) { + *error_r = t_strdup_printf("service(%s): " + "process_min_avail is higher than process_limit", + service->name); + return FALSE; + } + if (service->vsz_limit < 1024*1024 && service->vsz_limit != 0) { + *error_r = t_strdup_printf("service(%s): " + "vsz_limit is too low", service->name); + return FALSE; + } + +#ifdef CONFIG_BINARY + default_service = + master_default_settings_get_service(service->name); + if (default_service != NULL && + default_service->process_limit_1 && process_limit > 1) { + *error_r = t_strdup_printf("service(%s): " + "process_limit must be 1", service->name); + return FALSE; + } +#else + if (max_client_limit < service->client_limit) { + max_client_limit = service->client_limit; + max_client_limit_source = t_strdup_printf( + "service %s { client_limit }", service->name); + } +#endif + + if (*service->protocol != '\0') { + /* each imap/pop3/lmtp process can use up a connection, + although if service_count=1 it's only temporary. + imap-hibernate doesn't do any auth lookups. */ + if ((service->service_count != 1 || + strcmp(service->type, "login") == 0) && + strcmp(service->name, "imap-hibernate") != 0) { + str_printfa(max_auth_client_processes_reason, + " + service %s { process_limit=%u }", + service->name, process_limit); + max_auth_client_processes += process_limit; + } + } + if (strcmp(service->type, "login") == 0 || + strcmp(service->name, "auth") == 0) { + max_anvil_client_processes += process_limit; + str_printfa(max_anvil_client_processes_reason, + " + service %s { process_limit=%u }", + service->name, process_limit); + } + + if (!fix_file_listener_paths(&service->unix_listeners, pool, + set, &all_listeners, error_r)) { + *error_r = t_strdup_printf("service(%s): unix_listener: %s", + service->name, *error_r); + return FALSE; + } + if (!fix_file_listener_paths(&service->fifo_listeners, pool, + set, &all_listeners, error_r)) { + *error_r = t_strdup_printf("service(%s): fifo_listener: %s", + service->name, *error_r); + return FALSE; + } + add_inet_listeners(&service->inet_listeners, &all_listeners); + } + + client_limit = service_get_client_limit(set, "auth"); + if (client_limit < max_auth_client_processes && !warned_auth) { + warned_auth = TRUE; + str_delete(max_auth_client_processes_reason, 0, 3); + i_warning("service auth { client_limit=%u } is lower than " + "required under max. load (%u). " + "Counted for protocol services with service_count != 1: %s", + client_limit, max_auth_client_processes, + str_c(max_auth_client_processes_reason)); + } + + client_limit = service_get_client_limit(set, "anvil"); + if (client_limit < max_anvil_client_processes && !warned_anvil) { + warned_anvil = TRUE; + str_delete(max_anvil_client_processes_reason, 0, 3); + i_warning("service anvil { client_limit=%u } is lower than " + "required under max. load (%u). Counted with: %s", + client_limit, max_anvil_client_processes, + str_c(max_anvil_client_processes_reason)); + } +#ifndef CONFIG_BINARY + if (restrict_get_fd_limit(&fd_limit) == 0 && + fd_limit < (rlim_t)max_client_limit) { + i_warning("fd limit (ulimit -n) is lower than required " + "under max. load (%u < %u), because of %s", + (unsigned int)fd_limit, max_client_limit, + max_client_limit_source); + } +#endif + + /* check for duplicate listeners */ + array_sort(&all_listeners, i_strcmp_p); + strings = array_get(&all_listeners, &count); + for (i = 1; i < count; i++) { + if (strcmp(strings[i-1], strings[i]) == 0) { + *error_r = t_strdup_printf("duplicate listener: %s", + strings[i]); + return FALSE; + } + } + return TRUE; +} +/* </settings checks> */ + +static bool +login_want_core_dumps(const struct master_settings *set, gid_t *gid_r) +{ + struct service_settings *service; + const char *error; + bool cores = FALSE; + uid_t uid; + + *gid_r = (gid_t)-1; + + array_foreach_elem(&set->services, service) { + if (service->parsed_type == SERVICE_TYPE_LOGIN) { + if (service->login_dump_core) + cores = TRUE; + (void)get_uidgid(service->user, &uid, gid_r, &error); + if (*service->group != '\0') + (void)get_gid(service->group, gid_r, &error); + } + } + return cores; +} + +static bool +settings_have_auth_unix_listeners_in(const struct master_settings *set, + const char *dir) +{ + struct service_settings *service; + struct file_listener_settings *u; + size_t dir_len = strlen(dir); + + array_foreach_elem(&set->services, service) { + if (array_is_created(&service->unix_listeners)) { + array_foreach_elem(&service->unix_listeners, u) { + if (strncmp(u->path, dir, dir_len) == 0 && + u->path[dir_len] == '/') + return TRUE; + } + } + } + return FALSE; +} + +static void unlink_sockets(const char *path, const char *prefix) +{ + DIR *dirp; + struct dirent *dp; + struct stat st; + string_t *str; + size_t prefix_len; + + dirp = opendir(path); + if (dirp == NULL) { + i_error("opendir(%s) failed: %m", path); + return; + } + + prefix_len = strlen(prefix); + str = t_str_new(256); + while ((dp = readdir(dirp)) != NULL) { + if (dp->d_name[0] == '.') + continue; + + if (strncmp(dp->d_name, prefix, prefix_len) != 0) + continue; + + str_truncate(str, 0); + str_printfa(str, "%s/%s", path, dp->d_name); + if (lstat(str_c(str), &st) < 0) { + if (errno != ENOENT) + i_error("lstat(%s) failed: %m", str_c(str)); + continue; + } + if (!S_ISSOCK(st.st_mode)) + continue; + + /* try to avoid unlinking sockets if someone's already + listening in them. do this only at startup, because + when SIGHUPing a child process might catch the new + connection before it notices that it's supposed + to die. */ + if (!startup_finished) { + int fd = net_connect_unix(str_c(str)); + if (fd != -1 || errno != ECONNREFUSED) { + i_fatal("Dovecot is already running? " + "Socket already exists: %s", + str_c(str)); + } + } + + i_unlink_if_exists(str_c(str)); + } + (void)closedir(dirp); +} + +static void +mkdir_login_dir(const struct master_settings *set, const char *login_dir) +{ + mode_t mode; + gid_t gid; + + if (settings_have_auth_unix_listeners_in(set, login_dir)) { + /* we are not using external authentication, so make sure the + login directory exists with correct permissions and it's + empty. with external auth we wouldn't want to delete + existing sockets or break the permissions required by the + auth server. */ + mode = login_want_core_dumps(set, &gid) ? 0770 : 0750; + if (safe_mkdir(login_dir, mode, master_uid, gid) == 0) { + i_warning("Corrected permissions for login directory " + "%s", login_dir); + } + + unlink_sockets(login_dir, ""); + } else { + /* still make sure that login directory exists */ + if (mkdir(login_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", login_dir); + } +} + +void master_settings_do_fixes(const struct master_settings *set) +{ + const char *empty_dir; + struct stat st; + + /* since base dir is under /var/run by default, it may have been + deleted. */ + if (mkdir_parents(set->base_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", set->base_dir); + /* allow base_dir to be a symlink, so don't use lstat() */ + if (stat(set->base_dir, &st) < 0) + i_fatal("stat(%s) failed: %m", set->base_dir); + if (!S_ISDIR(st.st_mode)) + i_fatal("%s is not a directory", set->base_dir); + if ((st.st_mode & 0755) != 0755) { + i_warning("Fixing permissions of %s to be world-readable", + set->base_dir); + if (chmod(set->base_dir, 0755) < 0) + i_error("chmod(%s) failed: %m", set->base_dir); + } + + /* Make sure our permanent state directory exists */ + if (mkdir_parents(set->state_dir, 0755) < 0 && errno != EEXIST) + i_fatal("mkdir(%s) failed: %m", set->state_dir); + + mkdir_login_dir(set, t_strconcat(set->base_dir, "/login", NULL)); + mkdir_login_dir(set, t_strconcat(set->base_dir, "/token-login", NULL)); + + empty_dir = t_strconcat(set->base_dir, "/empty", NULL); + if (safe_mkdir(empty_dir, 0755, master_uid, getegid()) == 0) { + i_warning("Corrected permissions for empty directory " + "%s", empty_dir); + } +} |