SSLproxy/sys.c
Daniel Roethlisberger e384d89b35 Replace percent in IPv6 addrs in filenames
Percent is used to specify the interface for link-local addresses.
Even though this is not strictly necessary for NTFS, it makes sense to
replace percent with underscore as well to have cleaner filenames.

Also add some unit tests for sys_ip46str_sanitize() that actually test
the intended behaviour.
2015-03-15 20:08:11 +01:00

843 lines
19 KiB
C

/*
* SSLsplit - transparent SSL/TLS interception
* Copyright (c) 2009-2015, 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_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;
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_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,
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_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.
* FIXME - there is no facility to return errors from the file handlers
*/
int
sys_dir_eachfile(const char *dirname, sys_dir_eachfile_cb_t cb, void *arg)
{
FTS *tree;
FTSENT *node;
char * paths[2];
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));
return -1;
}
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) {
cb(node->fts_path, arg);
}
}
if (errno) {
log_err_printf("Error reading directory entry: %s\n",
strerror(errno));
return -1;
}
fts_close(tree);
free(paths[0]);
return 0;
}
/*
* 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: */