SSLproxy/sys.c

850 lines
20 KiB
C

/*
* SSLsplit - transparent SSL/TLS interception
* Copyright (c) 2009-2016, Daniel Roethlisberger <daniel@roe.ch>
* All rights reserved.
* http://www.roe.ch/SSLsplit
*
* 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 AUTHOR ``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 AUTHOR 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_level_printf(LOG_CRIT, "Failed to getgrnam group '%s': %s\n",
groupname, strerror(errno));
goto error;
}
}
if (username) {
errno = 0;
if (!(pw = getpwnam(username))) {
log_err_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "Failed to initgroups user '%s': %s\n",
username, strerror(errno));
goto error;
}
}
if (jaildir) {
if (chroot(jaildir) == -1) {
log_err_level_printf(LOG_CRIT, "Failed to chroot to '%s': %s\n",
jaildir, strerror(errno));
goto error;
}
if (chdir("/") == -1) {
log_err_level_printf(LOG_CRIT, "Failed to chdir to '/': %s\n",
strerror(errno));
goto error;
}
}
if (username) {
if (setgid(pw->pw_gid) == -1) {
log_err_level_printf(LOG_CRIT, "Failed to setgid to %i: %s\n",
pw->pw_gid, strerror(errno));
goto error;
}
if (setuid(pw->pw_uid) == -1) {
log_err_level_printf(LOG_CRIT, "Failed to setuid to %i: %s\n",
pw->pw_uid, strerror(errno));
goto error;
}
} else if (getuid() != geteuid()) {
if (setuid(getuid()) == -1) {
log_err_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "Failed to open '%s': %s\n", fn,
strerror(errno));
return -1;
}
if (flock(fd, LOCK_EX|LOCK_NB) == -1) {
log_err_level_printf(LOG_CRIT, "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;
rv = snprintf(pidbuf, sizeof(pidbuf), "%d\n", getpid());
if (rv == -1 || rv >= (int)sizeof(pidbuf))
return -1;
write(fd, pidbuf, strlen(pidbuf));
fsync(fd);
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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,
char *naddr, 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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "Cannot allocate memory\n");
return -1;
}
rv = getnameinfo(addr, addrlen,
tmphost, sizeof(tmphost),
*serv, 6,
NI_NUMERICHOST | NI_NUMERICSERV);
if (rv != 0) {
log_err_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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_level_printf(LOG_CRIT, "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;
if (addrlen == sizeof(sa_family_t)) {
asprintf(&name, "unnmd");
} else if (sun->sun_path[0] == '\0') {
/* abstract sockets is a Linux feature */
asprintf(&name, "abstr:%02x:%02x:%02x:%02x",
sun->sun_path[1],
sun->sun_path[2],
sun->sun_path[3],
sun->sun_path[4]);
} else {
asprintf(&name, "pname:%s", sun->sun_path);
}
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;
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 {
asprintf(&laddrstr, "[%s]:%s",
host, port);
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 {
asprintf(&faddrstr, "[%s]:%s",
host, port);
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: */