diff options
Diffstat (limited to 'src/sys.cc')
| -rw-r--r-- | src/sys.cc | 865 |
1 files changed, 865 insertions, 0 deletions
diff --git a/src/sys.cc b/src/sys.cc new file mode 100644 index 0000000..dee9549 --- /dev/null +++ b/src/sys.cc @@ -0,0 +1,865 @@ +/*- + * SSLsplit - transparent SSL/TLS interception + * https://www.roe.ch/SSLsplit + * + * Copyright (c) 2009-2018, Daniel Roethlisberger <[email protected]>. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "sys.h" + +#include "log.h" +#include "defaults.h" + +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/uio.h> +#include <sys/stat.h> +#include <sys/file.h> +#include <sys/un.h> +#include <netinet/in.h> +#include <netdb.h> +#include <fcntl.h> +#include <pwd.h> +#include <grp.h> +#include <fts.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <errno.h> + +#ifndef _SC_NPROCESSORS_ONLN +#include <sys/sysctl.h> +#endif /* !_SC_NPROCESSORS_ONLN */ + +#if HAVE_DARWIN_LIBPROC +#include <libproc.h> +#endif + +#include <event2/util.h> + +/* + * Permanently drop from root privileges to an unprivileged user account. + * Sets the real, effective and stored user and group ID and the list of + * ancillary groups. This is only safe if the effective user ID is 0. + * If username is unset and the effective uid != uid, drop privs to uid. + * This is to support setuid bit configurations. + * If groupname is set, it will be used instead of the user's default primary + * group. + * If jaildir is set, also chroot to jaildir after reading system files + * but before dropping privileges. + * Returns 0 on success, -1 on failure. + */ +int +sys_privdrop(const char *username, const char *groupname, const char *jaildir) +{ + struct passwd *pw = NULL; + struct group *gr = NULL; + int ret = -1; + + if (groupname) { + errno = 0; + if (!(gr = getgrnam(groupname))) { + log_err_printf("Failed to getgrnam group '%s': %s\n", + groupname, strerror(errno)); + goto error; + } + } + + if (username) { + errno = 0; + if (!(pw = getpwnam(username))) { + log_err_printf("Failed to getpwnam user '%s': %s\n", + username, strerror(errno)); + goto error; + } + + if (gr != NULL) { + pw->pw_gid = gr->gr_gid; + } + + if (initgroups(username, pw->pw_gid) == -1) { + log_err_printf("Failed to initgroups user '%s': %s\n", + username, strerror(errno)); + goto error; + } + } + + if (jaildir) { + if (chroot(jaildir) == -1) { + log_err_printf("Failed to chroot to '%s': %s\n", + jaildir, strerror(errno)); + goto error; + } + if (chdir("/") == -1) { + log_err_printf("Failed to chdir to '/': %s\n", + strerror(errno)); + goto error; + } + } + + if (username) { + if (setgid(pw->pw_gid) == -1) { + log_err_printf("Failed to setgid to %i: %s\n", + pw->pw_gid, strerror(errno)); + goto error; + } + if (setuid(pw->pw_uid) == -1) { + log_err_printf("Failed to setuid to %i: %s\n", + pw->pw_uid, strerror(errno)); + goto error; + } + } else if (getuid() != geteuid()) { + if (setuid(getuid()) == -1) { + log_err_printf("Failed to setuid(getuid()): %s\n", + strerror(errno)); + goto error; + } + } + + ret = 0; +error: + if (pw) { + endpwent(); + } + return ret; +} + +/* + * Returns 1 if username can be loaded from user database, 0 otherwise. + */ +int +sys_isuser(const char *username) +{ + errno = 0; + if (!getpwnam(username)) { + if (errno != 0 && errno != ENOENT) { + log_err_printf("Failed to load user '%s': %s (%i)\n", + username, strerror(errno), errno); + } + return 0; + } + + endpwent(); + return 1; +} + +/* + * Returns 1 if groupname can be loaded from group database, 0 otherwise. + */ +int +sys_isgroup(const char *groupname) +{ + errno = 0; + if (!getgrnam(groupname)) { + if (errno != 0 && errno != ENOENT) { + log_err_printf("Failed to load group '%s': %s (%i)\n", + groupname, strerror(errno), errno); + } + return 0; + } + return 1; +} + +/* + * Open and lock process ID file fn. + * Returns open file descriptor on success or -1 on errors. + */ +int +sys_pidf_open(const char *fn) +{ + int fd; + + if ((fd = open(fn, O_RDWR|O_CREAT, DFLT_PIDFMODE)) == -1) { + log_err_printf("Failed to open '%s': %s\n", fn, + strerror(errno)); + return -1; + } + if (flock(fd, LOCK_EX|LOCK_NB) == -1) { + log_err_printf("Failed to lock '%s': %s\n", fn, + strerror(errno)); + close(fd); + return -1; + } + + return fd; +} + +/* + * Write process ID to open process ID file descriptor fd. + * Returns 0 on success, -1 on errors. + */ +int +sys_pidf_write(int fd) +{ + char pidbuf[4*sizeof(pid_t)]; + int rv; + ssize_t n; + + rv = snprintf(pidbuf, sizeof(pidbuf), "%d\n", getpid()); + if (rv == -1 || rv >= (int)sizeof(pidbuf)) + return -1; + + n = write(fd, pidbuf, strlen(pidbuf)); + if (n < (ssize_t)strlen(pidbuf)) + return -1; + + rv = fsync(fd); + if (rv == -1) + return -1; + + rv = fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC); + if (rv == -1) + return -1; + + return 0; +} + +/* + * Close and remove open process ID file before quitting. + */ +void +sys_pidf_close(int fd, const char *fn) +{ + unlink(fn); + close(fd); +} + +/* + * Converts a local uid into a printable string representation. + * Returns an allocated buffer which must be freed by caller, or NULL on error. + */ +char * +sys_user_str(uid_t uid) +{ + static int bufsize = 0; + + if (!bufsize) { + /* on some platforms this compiles, but does not succeed */ + if ((bufsize = sysconf(_SC_GETPW_R_SIZE_MAX)) == -1) { + bufsize = 64; + } + } + + char *buf, *newbuf; + struct passwd pwd, *result = NULL; + int rv; + char *name; + + if (!(buf = malloc(bufsize))) + return NULL; + + do { + rv = getpwuid_r(uid, &pwd, buf, bufsize, &result); + if (rv == 0) { + if (result) { + name = strdup(pwd.pw_name); + free(buf); + return name; + } + free(buf); + + /* no entry found; return the integer representation */ + if (asprintf(&name, "%llu", (long long) uid) < 0) { + return NULL; + } + return name; + } + bufsize *= 2; + if (!(newbuf = realloc(buf, bufsize))) { + free(buf); + return NULL; + } + buf = newbuf; + } while (rv == ERANGE); + + free(buf); + log_err_printf("Failed to lookup uid: %s (%i)\n", strerror(rv), rv); + return NULL; +} + +/* + * Converts a local gid into a printable string representation. + * Returns an allocated buffer which must be freed by caller, or NULL on error. + */ +char * +sys_group_str(gid_t gid) +{ + static int bufsize = 0; + + if (!bufsize) { + /* on some platforms this compiles, but does not succeed */ + if ((bufsize = sysconf(_SC_GETGR_R_SIZE_MAX)) == -1) { + bufsize = 64; + } + } + + char *buf, *newbuf; + struct group grp, *result = NULL; + int rv; + char *name; + + if (!(buf = malloc(bufsize))) + return NULL; + + do { + rv = getgrgid_r(gid, &grp, buf, bufsize, &result); + if (rv == 0) { + if (result) { + name = strdup(grp.gr_name); + free(buf); + return name; + } + free(buf); + + /* no entry found; return the integer representation */ + if (asprintf(&name, "%llu", (long long) gid) < 0) { + return NULL; + } + return name; + } + bufsize *= 2; + if (!(newbuf = realloc(buf, bufsize))) { + free(buf); + return NULL; + } + buf = newbuf; + } while (rv == ERANGE); + + free(buf); + log_err_printf("Failed to lookup gid: %s (%i)\n", strerror(rv), rv); + return NULL; +} + +/* + * Parse an ascii host/IP and port tuple into a sockaddr_storage. + * On success, returns address family and fills in addr, addrlen. + * Returns -1 on error. + */ +int sys_sockaddr_parse(struct sockaddr_storage *addr, socklen_t *addrlen, + const char *naddr, const char *nport, int af, int flags) +{ + struct evutil_addrinfo hints; + struct evutil_addrinfo *ai; + int rv; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = af; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = EVUTIL_AI_ADDRCONFIG | flags; + rv = evutil_getaddrinfo(naddr, nport, &hints, &ai); + if (rv != 0) { + log_err_printf("Cannot resolve address '%s' port '%s': %s\n", + naddr, nport, gai_strerror(rv)); + return -1; + } + memcpy(addr, ai->ai_addr, ai->ai_addrlen); + *addrlen = ai->ai_addrlen; + af = ai->ai_family; + freeaddrinfo(ai); + return af; +} + +/* + * Converts an IPv4/IPv6 sockaddr into printable string representations of the + * host and the service (port) part. Writes allocated buffers to *host and + * *serv which must both be freed by the caller. Neither *host nor *port are + * freed by this function before newly allocating. + * Returns 0 on success, -1 otherwise. When -1 is returned, pointers in *host + * and *serv are invalid and must not be used nor freed by the caller. + */ +int +sys_sockaddr_str(struct sockaddr *addr, socklen_t addrlen, + char **host, char **serv) +{ + char tmphost[INET6_ADDRSTRLEN]; + int rv; + size_t hostsz; + + *serv = malloc(6); /* max decimal digits of short plus terminator */ + if (!*serv) { + log_err_printf("Cannot allocate memory\n"); + return -1; + } + rv = getnameinfo(addr, addrlen, + tmphost, sizeof(tmphost), + *serv, 6, + NI_NUMERICHOST | NI_NUMERICSERV); + if (rv != 0) { + log_err_printf("Cannot get nameinfo for socket address: %s\n", + gai_strerror(rv)); + free(*serv); + return -1; + } + hostsz = strlen(tmphost) + 1; /* including terminator */ + *host = malloc(hostsz); + if (!*host) { + log_err_printf("Cannot allocate memory\n"); + free(*serv); + return -1; + } + memcpy(*host, tmphost, hostsz); + return 0; +} + +/* + * Sanitizes a valid IPv4 or IPv6 address for use in a filename, i.e. removes + * characters that are invalid on NTFS and replaces them with more innocent + * characters. The function assumes that the input is a valid IPv4 or IPv6 + * address; it is not a generic filename sanitizer. + * + * Returns a copy of string s that must be freed by the caller. + * + * Invalid NTFS characters are < > : " / \ | ? * according to + * https://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247.aspx + */ +char * +sys_ip46str_sanitize(const char *s) +{ + char *copy, *p; + + copy = strdup(s); + if (!copy) + return NULL; + p = copy; + while (*p) { + switch (*p) { + case ':': + case '%': + *p = '_'; + break; + } + p++; + } + + return copy; +} + +/* + * Returns 1 if path points to an existing directory node in the filesystem. + * Returns 0 if path is NULL, does not exist, or points to a file of some kind. + */ +int +sys_isdir(const char *path) +{ + struct stat s; + + if (stat(path, &s) == -1) { + if (errno != ENOENT) { + log_err_printf("Error stating file: %s (%i)\n", + strerror(errno), errno); + } + return 0; + } + if (s.st_mode & S_IFDIR) + return 1; + return 0; +} + +/* + * Create directory including parent directories with mode_t. + * Mode of existing parent directories is not changed. + * Returns 0 on success, -1 and sets errno on error. + */ +int +sys_mkpath(const char *path, mode_t mode) +{ + char parent[strlen(path)+1]; + char *p; + + memcpy(parent, path, sizeof(parent)); + + p = parent; + do { + /* skip leading '/' characters */ + while (*p == '/') p++; + p = strchr(p, '/'); + if (p) { + /* overwrite '/' to terminate the string at the next + * parent directory */ + *p = '\0'; + } + + struct stat sbuf; + if (stat(parent, &sbuf) == -1) { + if (errno == ENOENT) { + if (mkdir(parent, mode) != 0) + return -1; + } else { + return -1; + } + } else if (!S_ISDIR(sbuf.st_mode)) { + errno = ENOTDIR; + return -1; + } + + if (p) { + /* replace the overwritten slash */ + *p = '/'; + p++; + } + } while (p); + + return 0; +} + +/* + * Iterate over all files in a directory hierarchy, calling the callback + * cb for each file, passing the filename and arg as arguments. Files and + * directories beginning with a dot are skipped, symlinks are followed. + */ +int +sys_dir_eachfile(const char *dirname, sys_dir_eachfile_cb_t cb, void *arg) +{ + FTS *tree; + FTSENT *node; + char * paths[2]; + int rv = 0; + + paths[1] = NULL; + paths[0] = strdup(dirname); + if (!paths[0]) + return -1; + + tree = fts_open(paths, FTS_NOCHDIR | FTS_LOGICAL, NULL); + if (!tree) { + log_err_printf("Cannot open directory '%s': %s\n", + dirname, strerror(errno)); + rv = -1; + goto out1; + } + + while ((node = fts_read(tree))) { + if (node->fts_level > 0 && node->fts_name[0] == '.') + fts_set(tree, node, FTS_SKIP); + else if (node->fts_info & FTS_F) { + rv = cb(node->fts_path, arg); + if (rv == -1) + goto out2; + } + } + if (errno) { + log_err_printf("Error reading directory entry: %s\n", + strerror(errno)); + rv = -1; + goto out2; + } + +out2: + fts_close(tree); + +out1: + free(paths[0]); + return rv; +} + +/* + * Portably get the number of CPU cores online in the system. + */ +uint32_t +sys_get_cpu_cores(void) +{ +#ifdef _SC_NPROCESSORS_ONLN + return sysconf(_SC_NPROCESSORS_ONLN); +#else /* !_SC_NPROCESSORS_ONLN */ + int mib[2]; + uint32_t n; + size_t len = sizeof(n); + + mib[0] = CTL_HW; + mib[1] = HW_AVAILCPU; + sysctl(mib, sizeof(mib)/sizeof(int), &n, &len, NULL, 0); + + if (n < 1) { + mib[1] = HW_NCPU; + sysctl(mib, sizeof(mib)/sizeof(int), &n, &len, NULL, 0); + if (n < 1) { + n = 1; + } + } + return n; +#endif /* !_SC_NPROCESSORS_ONLN */ +} + +/* + * Send a message and optional file descriptor on a connected AF_UNIX + * SOCKET_DGRAM socket s. Returns the return value of sendmsg(). + * If fd is -1, no file descriptor is passed. + */ +ssize_t +sys_sendmsgfd(int sock, void *buf, size_t bufsz, int fd) +{ + struct iovec iov; + struct msghdr msg; + struct cmsghdr *cmsg; + char cmsgbuf[CMSG_SPACE(sizeof(int))]; + ssize_t n; + + iov.iov_base = buf; + iov.iov_len = bufsz; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + if (fd != -1) { + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + + cmsg = CMSG_FIRSTHDR(&msg); + if (!cmsg) + return -1; + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + + *((int *) CMSG_DATA(cmsg)) = fd; + } else { + msg.msg_control = NULL; + msg.msg_controllen = 0; + } + do { +#ifdef MSG_NOSIGNAL + n = sendmsg(sock, &msg, MSG_NOSIGNAL); +#else /* !MSG_NOSIGNAL */ + n = sendmsg(sock, &msg, 0); +#endif /* !MSG_NOSIGNAL */ + } while (n == -1 && errno == EINTR); + return n; +} + +/* + * Receive a message and optional file descriptor on a connected AF_UNIX + * SOCKET_DGRAM socket s. Returns the return value of recvmsg()/recv() + * and sets errno to EINVAL if the received message is malformed. + * If pfd is NULL, no file descriptor is received; if a file descriptor was + * part of the received message and pfd is NULL, then the kernel will close it. + */ +ssize_t +sys_recvmsgfd(int sock, void *buf, size_t bufsz, int *pfd) +{ + ssize_t n; + + if (pfd) { + struct iovec iov; + struct msghdr msg; + struct cmsghdr *cmsg; + unsigned char cmsgbuf[CMSG_SPACE(sizeof(int))]; + + iov.iov_base = buf; + iov.iov_len = bufsz; + + msg.msg_name = NULL; + msg.msg_namelen = 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + do { + n = recvmsg(sock, &msg, 0); + } while (n == -1 && errno == EINTR); + if (n <= 0) + return n; + cmsg = CMSG_FIRSTHDR(&msg); + if (cmsg && cmsg->cmsg_len == CMSG_LEN(sizeof(int))) { + if (cmsg->cmsg_level != SOL_SOCKET) { + errno = EINVAL; + return -1; + } + if (cmsg->cmsg_type != SCM_RIGHTS) { + errno = EINVAL; + return -1; + } + *pfd = *((int *) CMSG_DATA(cmsg)); + } else { + *pfd = -1; + } + } else { + do { + n = recv(sock, buf, bufsz, 0); + } while (n == -1 && errno == EINTR); + } + return n; +} + +/* + * Format AF_UNIX socket address into printable string. + * Returns newly allocated string that must be freed by caller. + */ +static char * +sys_afunix_str(struct sockaddr *addr, socklen_t addrlen) +{ + struct sockaddr_un *sun = (struct sockaddr_un *)addr; + char *name; + int rv; + + if (addrlen == sizeof(sa_family_t)) { + rv = asprintf(&name, "unnmd"); + } else if (sun->sun_path[0] == '\0') { + /* abstract sockets is a Linux feature */ + rv = asprintf(&name, "abstr:%02x:%02x:%02x:%02x", + sun->sun_path[1], + sun->sun_path[2], + sun->sun_path[3], + sun->sun_path[4]); + } else { + rv = asprintf(&name, "pname:%s", sun->sun_path); + } + if (rv == -1) + name = NULL; + return name; +} + +/* + * Dump all open file descriptors to stdout - poor man's lsof/fstat/sockstat + */ +void +sys_dump_fds(void) +{ + int maxfd = 0; + +#ifdef F_MAXFD + if (!maxfd && ((maxfd = fcntl(0, F_MAXFD)) == -1)) { + fprintf(stderr, "fcntl(0, F_MAXFD) failed: %s (%i)\n", + strerror(errno), errno); + } +#endif /* F_MAXFD */ +#ifdef _SC_OPEN_MAX + if (!maxfd && ((maxfd = sysconf(_SC_OPEN_MAX)) == -1)) { + fprintf(stderr, "sysconf(_SC_OPEN_MAX) failed: %s (%i)\n", + strerror(errno), errno); + } +#endif /* _SC_OPEN_MAX */ + if (!maxfd) + maxfd = 65535; + + for (int fd = 0; fd <= maxfd; fd++) { + struct stat st; + + if (fstat(fd, &st) == -1) { + continue; + } + + printf("%5d:", fd); + switch (st.st_mode & S_IFMT) { + case S_IFBLK: printf(" blkdev"); break; + case S_IFCHR: printf(" chrdev"); break; + case S_IFDIR: printf(" dir "); break; + case S_IFIFO: printf(" fifo "); break; + case S_IFLNK: printf(" lnkfil"); break; + case S_IFREG: printf(" regfil"); break; + case S_IFSOCK: printf(" socket"); break; + default: printf(" unknwn"); break; + } + + if ((st.st_mode & S_IFMT) == S_IFSOCK) { + int lrv, frv, arv; + struct sockaddr_storage lss, fss; + socklen_t lsslen = sizeof(lss); + socklen_t fsslen = sizeof(fss); + char *laddrstr, *faddrstr; + + lrv = getsockname(fd, (struct sockaddr *)&lss, &lsslen); + frv = getpeername(fd, (struct sockaddr *)&fss, &fsslen); + + switch (lss.ss_family) { + case AF_INET: + case AF_INET6: { + if (lrv == 0) { + char *host, *port; + if (sys_sockaddr_str( + (struct sockaddr *)&lss, + lsslen, + &host, &port) != 0) { + laddrstr = strdup("?"); + } else { + arv = asprintf(&laddrstr, + "[%s]:%s", + host, port); + if (arv == -1) + laddrstr = NULL; + free(host); + free(port); + } + } else { + laddrstr = strdup("n/a"); + } + if (frv == 0) { + char *host, *port; + if (sys_sockaddr_str( + (struct sockaddr *)&fss, + fsslen, + &host, &port) != 0) { + faddrstr = strdup("?"); + } else { + arv = asprintf(&faddrstr, + "[%s]:%s", + host, port); + if (arv == -1) + faddrstr = NULL; + free(host); + free(port); + } + } else { + faddrstr = strdup("n/a"); + } + printf(" %-6s %s -> %s", + lss.ss_family == AF_INET ? "in" : "in6", + laddrstr, faddrstr); + free(laddrstr); + free(faddrstr); + break; + } + case AF_UNIX: { + if (lrv == 0) { + laddrstr = sys_afunix_str((struct sockaddr *)&lss, lsslen); + } else { + laddrstr = strdup("n/a"); + } + if (frv == 0) { + faddrstr = sys_afunix_str((struct sockaddr *)&fss, fsslen); + } else { + faddrstr = strdup("n/a"); + } + printf(" unix %s -> %s", laddrstr, faddrstr); + free(laddrstr); + free(faddrstr); + break; + } + case AF_UNSPEC: { + printf(" unspec"); + break; + } + default: + printf(" (%i)", lss.ss_family); + } + } + printf("\n"); + } +} + +/* vim: set noet ft=c: */ + |
