/* * Copyright (c) 2019 Todd C. Miller * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include "config.h" #include #include #include #include #include #include #include #include #include #ifdef HAVE_STDBOOL_H # include #else # include "compat/stdbool.h" #endif #include #include #include #include #include #include #ifndef HAVE_GETADDRINFO # include "compat/getaddrinfo.h" #endif #include "log_server.pb-c.h" #include "sudo_gettext.h" /* must be included before sudo_compat.h */ #include "sudo_compat.h" #include "sudo_debug.h" #include "sudo_util.h" #include "sudo_fatal.h" #include "sudo_iolog.h" #include "pathnames.h" #include "logsrvd.h" struct logsrvd_config; typedef bool (*logsrvd_conf_cb_t)(struct logsrvd_config *config, const char *); struct logsrvd_config_entry { char *conf_str; logsrvd_conf_cb_t setter; }; struct logsrvd_config_section { char *name; struct logsrvd_config_entry *entries; }; static struct logsrvd_config { struct logsrvd_config_server { struct listen_address_list addresses; } server; struct logsrvd_config_iolog { bool compress; bool flush; bool gid_set; uid_t uid; gid_t gid; mode_t mode; unsigned int maxseq; char *iolog_dir; char *iolog_file; } iolog; struct logsrvd_config_eventlog { enum logsrvd_eventlog_type log_type; enum logsrvd_eventlog_format log_format; } eventlog; struct logsrvd_config_syslog { unsigned int maxlen; int facility; int acceptpri; int rejectpri; int alertpri; } syslog; struct logsrvd_config_logfile { char *path; char *time_format; } logfile; } *logsrvd_config; /* iolog getters */ mode_t logsrvd_conf_iolog_mode(void) { return logsrvd_config->iolog.mode; } const char * logsrvd_conf_iolog_dir(void) { return logsrvd_config->iolog.iolog_dir; } const char * logsrvd_conf_iolog_file(void) { return logsrvd_config->iolog.iolog_file; } /* server getters */ struct listen_address_list * logsrvd_conf_listen_address(void) { return &logsrvd_config->server.addresses; } /* eventlog getters */ enum logsrvd_eventlog_type logsrvd_conf_eventlog_type(void) { return logsrvd_config->eventlog.log_type; } enum logsrvd_eventlog_format logsrvd_conf_eventlog_format(void) { return logsrvd_config->eventlog.log_format; } /* syslog getters */ unsigned int logsrvd_conf_syslog_maxlen(void) { return logsrvd_config->syslog.maxlen; } int logsrvd_conf_syslog_facility(void) { return logsrvd_config->syslog.facility; } int logsrvd_conf_syslog_acceptpri(void) { return logsrvd_config->syslog.acceptpri; } int logsrvd_conf_syslog_rejectpri(void) { return logsrvd_config->syslog.rejectpri; } int logsrvd_conf_syslog_alertpri(void) { return logsrvd_config->syslog.alertpri; } /* logfile getters */ const char * logsrvd_conf_logfile_path(void) { return logsrvd_config->logfile.path; } const char * logsrvd_conf_logfile_time_format(void) { return logsrvd_config->logfile.time_format; } /* I/O log callbacks */ static bool cb_iolog_dir(struct logsrvd_config *config, const char *path) { debug_decl(cb_iolog_dir, SUDO_DEBUG_UTIL) free(config->iolog.iolog_dir); if ((config->iolog.iolog_dir = strdup(path)) == NULL) { sudo_warn(NULL); debug_return_bool(false); } debug_return_bool(true); } static bool cb_iolog_file(struct logsrvd_config *config, const char *path) { debug_decl(cb_iolog_file, SUDO_DEBUG_UTIL) free(config->iolog.iolog_file); if ((config->iolog.iolog_file = strdup(path)) == NULL) { sudo_warn(NULL); debug_return_bool(false); } debug_return_bool(true); } static bool cb_iolog_compress(struct logsrvd_config *config, const char *str) { int val; debug_decl(cb_iolog_compress, SUDO_DEBUG_UTIL) if ((val = sudo_strtobool(str)) == -1) debug_return_bool(false); config->iolog.compress = val; debug_return_bool(true); } static bool cb_iolog_flush(struct logsrvd_config *config, const char *str) { int val; debug_decl(cb_iolog_flush, SUDO_DEBUG_UTIL) if ((val = sudo_strtobool(str)) == -1) debug_return_bool(false); config->iolog.flush = val; debug_return_bool(true); } static bool cb_iolog_user(struct logsrvd_config *config, const char *user) { struct passwd *pw; debug_decl(cb_iolog_user, SUDO_DEBUG_UTIL) if ((pw = getpwnam(user)) == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unknown user %s", user); debug_return_bool(false); } config->iolog.uid = pw->pw_uid; if (!config->iolog.gid_set) config->iolog.gid = pw->pw_gid; debug_return_bool(true); } static bool cb_iolog_group(struct logsrvd_config *config, const char *group) { struct group *gr; debug_decl(cb_iolog_group, SUDO_DEBUG_UTIL) if ((gr = getgrnam(group)) == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unknown group %s", group); debug_return_bool(false); } config->iolog.gid = gr->gr_gid; config->iolog.gid_set = true; debug_return_bool(true); } static bool cb_iolog_mode(struct logsrvd_config *config, const char *str) { const char *errstr; mode_t mode; debug_decl(cb_iolog_mode, SUDO_DEBUG_UTIL) mode = sudo_strtomode(str, &errstr); if (errstr != NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to parse iolog mode %s", str); debug_return_bool(false); } config->iolog.mode = mode; debug_return_bool(true); } static bool cb_iolog_maxseq(struct logsrvd_config *config, const char *str) { const char *errstr; unsigned int value; debug_decl(cb_iolog_maxseq, SUDO_DEBUG_UTIL) value = sudo_strtonum(str, 0, SESSID_MAX, &errstr); if (errstr != NULL) { if (errno != ERANGE) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "bad maxseq: %s: %s", str, errstr); debug_return_bool(false); } /* Out of range, clamp to SESSID_MAX as documented. */ value = SESSID_MAX; } config->iolog.maxseq = value; debug_return_bool(true); } /* Server callbacks */ /* TODO: unit test */ static bool cb_listen_address(struct logsrvd_config *config, const char *str) { struct addrinfo hints, *res, *res0 = NULL; char *copy, *host, *port; bool ret = false; int error; debug_decl(cb_iolog_mode, SUDO_DEBUG_UTIL) if ((copy = strdup(str)) == NULL) { sudo_warn(NULL); debug_return_bool(false); } host = copy; /* Check for IPv6 address like [::0] followed by optional port */ if (*host == '[') { host++; port = strchr(host, ']'); if (port == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "invalid IPv6 address %s", str); goto done; } *port++ = '\0'; if (*port == ':') { port++; } else if (*port == '\0') { port = NULL; /* no port specified */ } else { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "invalid IPv6 address %s", str); goto done; } } else { port = strrchr(host, ':'); if (port != NULL) *port++ = '\0'; } if (port == NULL) port = DEFAULT_PORT_STR; if (host[0] == '*' && host[1] == '\0') host = NULL; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_flags = AI_PASSIVE; error = getaddrinfo(host, port, &hints, &res0); if (error != 0) { sudo_warnx("%s", gai_strerror(error)); goto done; } for (res = res0; res != NULL; res = res->ai_next) { struct listen_address *addr; if ((addr = malloc(sizeof(*addr))) == NULL) { sudo_warn(NULL); goto done; } memcpy(&addr->sa_un, res->ai_addr, res->ai_addrlen); addr->sa_len = res->ai_addrlen; TAILQ_INSERT_TAIL(&config->server.addresses, addr, entries); } ret = true; done: if (res0 != NULL) freeaddrinfo(res0); free(copy); debug_return_bool(ret); } /* eventlog callbacks */ static bool cb_eventlog_type(struct logsrvd_config *config, const char *str) { debug_decl(cb_eventlog_type, SUDO_DEBUG_UTIL) if (strcmp(str, "none") == 0) config->eventlog.log_type = EVLOG_NONE; else if (strcmp(str, "syslog") == 0) config->eventlog.log_type = EVLOG_SYSLOG; else if (strcmp(str, "logfile") == 0) config->eventlog.log_type = EVLOG_FILE; else debug_return_bool(false); debug_return_bool(true); } static bool cb_eventlog_format(struct logsrvd_config *config, const char *str) { debug_decl(cb_eventlog_format, SUDO_DEBUG_UTIL) if (strcmp(str, "sudo") == 0) config->eventlog.log_format = EVLOG_SUDO; else debug_return_bool(false); debug_return_bool(true); } /* syslog callbacks */ static bool cb_syslog_maxlen(struct logsrvd_config *config, const char *str) { unsigned int maxlen; const char *errstr; debug_decl(cb_syslog_maxlen, SUDO_DEBUG_UTIL) maxlen = sudo_strtonum(str, 1, UINT_MAX, &errstr); if (errstr != NULL) debug_return_bool(false); config->syslog.maxlen = maxlen; debug_return_bool(true); } static bool cb_syslog_facility(struct logsrvd_config *config, const char *str) { int logfac; debug_decl(cb_syslog_facility, SUDO_DEBUG_UTIL) if (!sudo_str2logfac(str, &logfac)) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "invalid syslog priority %s", str); debug_return_bool(false); } config->syslog.facility = logfac; debug_return_bool(true); } static bool cb_syslog_acceptpri(struct logsrvd_config *config, const char *str) { int logpri; debug_decl(cb_syslog_acceptpri, SUDO_DEBUG_UTIL) if (!sudo_str2logpri(str, &logpri)) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "invalid syslog priority %s", str); debug_return_bool(false); } config->syslog.acceptpri = logpri; debug_return_bool(true); } static bool cb_syslog_rejectpri(struct logsrvd_config *config, const char *str) { int logpri; debug_decl(cb_syslog_rejectpri, SUDO_DEBUG_UTIL) if (!sudo_str2logpri(str, &logpri)) debug_return_bool(false); config->syslog.rejectpri = logpri; debug_return_bool(true); } static bool cb_syslog_alertpri(struct logsrvd_config *config, const char *str) { int logpri; debug_decl(cb_syslog_alertpri, SUDO_DEBUG_UTIL) if (!sudo_str2logpri(str, &logpri)) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "invalid syslog priority %s", str); debug_return_bool(false); } config->syslog.alertpri = logpri; debug_return_bool(true); } /* logfile callbacks */ static bool cb_logfile_path(struct logsrvd_config *config, const char *str) { char *copy = NULL; debug_decl(cb_logfile_path, SUDO_DEBUG_UTIL) if (*str != '/') { debug_return_bool(false); sudo_warnx(U_("%s: not a fully qualified path"), str); debug_return_bool(false); } if ((copy = strdup(str)) == NULL) { sudo_warn(NULL); debug_return_bool(false); } free(config->logfile.path); config->logfile.path = copy; debug_return_bool(true); } static bool cb_logfile_time_format(struct logsrvd_config *config, const char *str) { char *copy = NULL; debug_decl(cb_logfile_time_format, SUDO_DEBUG_UTIL) if ((copy = strdup(str)) == NULL) { sudo_warn(NULL); debug_return_bool(false); } free(config->logfile.time_format); config->logfile.time_format = copy; debug_return_bool(true); } static struct logsrvd_config_entry server_conf_entries[] = { { "listen_address", cb_listen_address }, { NULL } }; static struct logsrvd_config_entry iolog_conf_entries[] = { { "iolog_dir", cb_iolog_dir }, { "iolog_file", cb_iolog_file }, { "iolog_flush", cb_iolog_flush }, { "iolog_compress", cb_iolog_compress }, { "iolog_user", cb_iolog_user }, { "iolog_group", cb_iolog_group }, { "iolog_mode", cb_iolog_mode }, { "maxseq", cb_iolog_maxseq }, { NULL } }; static struct logsrvd_config_entry eventlog_conf_entries[] = { { "log_type", cb_eventlog_type }, { "log_format", cb_eventlog_format }, { NULL } }; static struct logsrvd_config_entry syslog_conf_entries[] = { { "maxlen", cb_syslog_maxlen }, { "facility", cb_syslog_facility }, { "reject_priority", cb_syslog_rejectpri }, { "accept_priority", cb_syslog_acceptpri }, { "alert_priority", cb_syslog_alertpri }, { NULL } }; static struct logsrvd_config_entry logfile_conf_entries[] = { { "path", cb_logfile_path }, { "time_format", cb_logfile_time_format }, { NULL } }; static struct logsrvd_config_section logsrvd_config_sections[] = { { "server", server_conf_entries }, { "iolog", iolog_conf_entries }, { "eventlog", eventlog_conf_entries }, { "syslog", syslog_conf_entries }, { "logfile", logfile_conf_entries }, { NULL } }; static bool logsrvd_conf_parse(struct logsrvd_config *config, FILE *fp, const char *path) { struct logsrvd_config_section *conf_section = NULL; unsigned int lineno = 0; size_t linesize = 0; char *line = NULL; bool ret = false; debug_decl(logsrvd_conf_parse, SUDO_DEBUG_UTIL) while (sudo_parseln(&line, &linesize, &lineno, fp, 0) != -1) { struct logsrvd_config_entry *entry; char *ep, *val; /* Skip blank, comment or invalid lines. */ if (*line == '\0' || *line == ';') continue; /* New section */ if (line[0] == '[') { char *section_name = line + 1; char *cp = strchr(section_name, ']'); if (cp == NULL) { sudo_warnx(U_("%s:%d unmatched '[': %s"), path, lineno, line); goto done; } *cp = '\0'; for (conf_section = logsrvd_config_sections; conf_section->name != NULL; conf_section++) { if (strcasecmp(section_name, conf_section->name) == 0) break; } if (conf_section->name == NULL) { sudo_warnx(U_("%s:%d invalid config section: %s"), path, lineno, section_name); goto done; } continue; } if ((ep = strchr(line, '=')) == NULL) { sudo_warnx(U_("%s:%d invalid configuration line: %s"), path, lineno, line); goto done; } if (conf_section == NULL) { sudo_warnx(U_("%s:%d expected section name: %s"), path, lineno, line); goto done; } val = ep + 1; while (isspace((unsigned char)*val)) val++; while (ep > line && isspace((unsigned char)ep[-1])) ep--; *ep = '\0'; for (entry = conf_section->entries; entry->conf_str != NULL; entry++) { if (strcasecmp(line, entry->conf_str) == 0) { if (!entry->setter(config, val)) { sudo_warnx(U_("invalid value for %s: %s"), entry->conf_str, val); goto done; } break; } } if (entry->conf_str == NULL) { sudo_warnx(U_("%s:%d unknown key: %s"), path, lineno, line); goto done; } } ret = true; done: free(line); debug_return_bool(ret); } /* Free the specified struct logsrvd_config and its contents. */ void logsrvd_conf_free(struct logsrvd_config *config) { struct listen_address *addr; debug_decl(logsrvd_conf_free, SUDO_DEBUG_UTIL) if (config == NULL) debug_return; /* struct logsrvd_config_server */ while ((addr = TAILQ_FIRST(&config->server.addresses))) { TAILQ_REMOVE(&config->server.addresses, addr, entries); free(addr); } /* struct logsrvd_config_iolog */ free(config->iolog.iolog_dir); free(config->iolog.iolog_file); /* struct logsrvd_config_logfile */ free(config->logfile.path); free(config->logfile.time_format); free(config); debug_return; } /* Allocate a new struct logsrvd_config and set default values. */ struct logsrvd_config * logsrvd_conf_alloc(void) { struct logsrvd_config *config; debug_decl(logsrvd_conf_alloc, SUDO_DEBUG_UTIL) if ((config = calloc(1, sizeof(*config))) == NULL) { sudo_warn(NULL); debug_return_ptr(NULL); } /* Server defaults */ TAILQ_INIT(&config->server.addresses); /* I/O log defaults */ config->iolog.compress = false; config->iolog.flush = true; config->iolog.mode = S_IRUSR|S_IWUSR; config->iolog.maxseq = SESSID_MAX; if (!cb_iolog_dir(config, _PATH_SUDO_IO_LOGDIR)) goto bad; if (!cb_iolog_file(config, "%{seq}")) goto bad; config->iolog.uid = ROOT_UID; config->iolog.gid = ROOT_GID; config->iolog.gid_set = false; /* Event log defaults */ config->eventlog.log_type = EVLOG_SYSLOG; config->eventlog.log_format = EVLOG_SUDO; /* Syslog defaults */ config->syslog.maxlen = 960; if (!cb_syslog_facility(config, LOGFAC)) { sudo_warnx(U_("unknown syslog facility %s"), LOGFAC); goto bad; } if (!cb_syslog_acceptpri(config, PRI_SUCCESS)) { sudo_warnx(U_("unknown syslog priority %s"), PRI_SUCCESS); goto bad; } if (!cb_syslog_rejectpri(config, PRI_FAILURE)) { sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE); goto bad; } if (!cb_syslog_alertpri(config, PRI_FAILURE)) { sudo_warnx(U_("unknown syslog priority %s"), PRI_FAILURE); goto bad; } /* Log file defaults */ if (!cb_logfile_time_format(config, "%h %e %T")) goto bad; if (!cb_logfile_path(config, _PATH_SUDO_LOGFILE)) goto bad; debug_return_ptr(config); bad: logsrvd_conf_free(config); debug_return_ptr(NULL); } bool logsrvd_conf_apply(struct logsrvd_config *config) { debug_decl(logsrvd_conf_apply, SUDO_DEBUG_UTIL) /* There can be multiple addresses so we can't set a default earlier. */ if (TAILQ_EMPTY(&config->server.addresses)) { if (!cb_listen_address(config, "*:30344")) debug_return_bool(false); } /* Set I/O log library settings */ iolog_set_defaults(); iolog_set_compress(config->iolog.compress); iolog_set_flush(config->iolog.flush); iolog_set_owner(config->iolog.uid, config->iolog.gid); iolog_set_mode(config->iolog.mode); iolog_set_maxseq(config->iolog.maxseq); logsrvd_conf_free(logsrvd_config); logsrvd_config = config; debug_return_bool(true); } /* * Read .ini style logsrvd.conf file. * Note that we use '#' not ';' for the comment character. */ bool logsrvd_conf_read(const char *path) { struct logsrvd_config *config; bool ret = false; FILE *fp = NULL; debug_decl(logsrvd_conf_read, SUDO_DEBUG_UTIL) config = logsrvd_conf_alloc(); if ((fp = fopen(path, "r")) == NULL) { if (errno != ENOENT) { sudo_warn("%s", path); goto done; } } else { if (!logsrvd_conf_parse(config, fp, path)) goto done; } /* Install new config */ if (logsrvd_conf_apply(config)) { config = NULL; ret = true; } done: logsrvd_conf_free(config); if (fp != NULL) fclose(fp); debug_return_bool(ret); }