root/sbin/isakmpd/monitor.c
/* $OpenBSD: monitor.c,v 1.83 2023/02/08 08:03:11 tb Exp $       */

/*
 * Copyright (c) 2003 HÃ¥kan Olsson.  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 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/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <netinet/in.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>

#include <regex.h>
#include <keynote.h>

#include "conf.h"
#include "log.h"
#include "monitor.h"
#include "policy.h"
#include "ui.h"
#include "util.h"
#include "pf_key_v2.h"

struct monitor_state {
        pid_t           pid;
        int             s;
        char            root[PATH_MAX];
} m_state;

extern char *pid_file;

extern void     set_slave_signals(void);

/* Private functions.  */
static void     must_read(void *, size_t);
static void     must_write(const void *, size_t);

static void     m_priv_getfd(void);
static void     m_priv_setsockopt(void);
static void     m_priv_req_readdir(void);
static void     m_priv_bind(void);
static void     m_priv_pfkey_open(void);
static int      m_priv_local_sanitize_path(const char *, size_t, int);
static int      m_priv_check_sockopt(int, int);
static int      m_priv_check_bind(const struct sockaddr *, socklen_t);

static void     set_monitor_signals(void);
static void     sig_pass_to_chld(int);

/*
 * Public functions, unprivileged.
 */

/* Setup monitor context, fork, drop child privs.  */
pid_t
monitor_init(int debug)
{
        struct passwd  *pw;
        int             p[2];

        bzero(&m_state, sizeof m_state);

        if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, p) != 0)
                log_fatal("monitor_init: socketpair() failed");

        pw = getpwnam(ISAKMPD_PRIVSEP_USER);
        if (pw == NULL)
                log_fatalx("monitor_init: getpwnam(\"%s\") failed",
                    ISAKMPD_PRIVSEP_USER);
        strlcpy(m_state.root, pw->pw_dir, sizeof m_state.root);

        set_monitor_signals();
        m_state.pid = fork();

        if (m_state.pid == -1)
                log_fatal("monitor_init: fork of unprivileged child failed");
        if (m_state.pid == 0) {
                /* The child process drops privileges. */
                set_slave_signals();

                if (chroot(pw->pw_dir) != 0 || chdir("/") != 0)
                        log_fatal("monitor_init: chroot failed");

                if (setgroups(1, &pw->pw_gid) == -1 ||
                    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
                    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
                        log_fatal("monitor_init: can't drop privileges");

                m_state.s = p[0];
                close(p[1]);

                LOG_DBG((LOG_MISC, 10,
                    "monitor_init: privileges dropped for child process"));
        } else {
                /* Privileged monitor. */
                setproctitle("monitor [priv]");

                m_state.s = p[1];
                close(p[0]);
        }

        /* With "-dd", stop and wait here. For gdb "attach" etc.  */
        if (debug > 1) {
                log_print("monitor_init: stopped %s PID %d fd %d%s",
                    m_state.pid ? "priv" : "child", getpid(), m_state.s,
                    m_state.pid ? ", waiting for SIGCONT" : "");
                kill(getpid(), SIGSTOP);        /* Wait here for SIGCONT.  */
                if (m_state.pid)
                        kill(m_state.pid, SIGCONT); /* Continue child.  */
        }

        return m_state.pid;
}

void
monitor_exit(int code)
{
        int status = 0, gotstatus = 0;
        pid_t pid;

        if (m_state.pid != 0) {
                /* When called from the monitor, kill slave and wait for it  */
                kill(m_state.pid, SIGTERM);

                do {
                        pid = waitpid(m_state.pid, &status, 0);
                } while (pid == -1 && errno == EINTR);
                if (pid != -1)
                        gotstatus = 1;

                /* Remove FIFO and pid files.  */
                unlink(ui_fifo);
                unlink(pid_file);
        }

        close(m_state.s);
        if (code == 0 && gotstatus)
                exit(WIFEXITED(status)? WEXITSTATUS(status) : 1);
        else
                exit(code);
}

int
monitor_pf_key_v2_open(void)
{
        int     err, cmd;

        cmd = MONITOR_PFKEY_OPEN;
        must_write(&cmd, sizeof cmd);

        must_read(&err, sizeof err);
        if (err < 0) {
                log_error("monitor_pf_key_v2_open: parent could not create "
                    "PF_KEY socket");
                return -1;
        }
        pf_key_v2_socket = mm_receive_fd(m_state.s);
        if (pf_key_v2_socket < 0) {
                log_error("monitor_pf_key_v2_open: mm_receive_fd() failed");
                return -1;
        }

        return pf_key_v2_socket;
}

int
monitor_open(const char *path, int flags, mode_t mode)
{
        size_t  len;
        int     fd, err, cmd;
        char    pathreal[PATH_MAX];

        if (path[0] == '/')
                strlcpy(pathreal, path, sizeof pathreal);
        else
                snprintf(pathreal, sizeof pathreal, "%s/%s", m_state.root,
                    path);

        cmd = MONITOR_GET_FD;
        must_write(&cmd, sizeof cmd);

        len = strlen(pathreal);
        must_write(&len, sizeof len);
        must_write(&pathreal, len);

        must_write(&flags, sizeof flags);
        must_write(&mode, sizeof mode);

        must_read(&err, sizeof err);
        if (err != 0) {
                errno = err;
                return -1;
        }

        fd = mm_receive_fd(m_state.s);
        if (fd < 0) {
                log_error("monitor_open: mm_receive_fd () failed");
                return -1;
        }

        return fd;
}

FILE *
monitor_fopen(const char *path, const char *mode)
{
        FILE    *fp;
        int      fd, flags = 0, saved_errno;
        mode_t   mask, cur_umask;

        /* Only the child process is supposed to run this.  */
        if (m_state.pid)
                log_fatal("[priv] bad call to monitor_fopen");

        switch (mode[0]) {
        case 'r':
                flags = (mode[1] == '+' ? O_RDWR : O_RDONLY);
                break;
        case 'w':
                flags = (mode[1] == '+' ? O_RDWR : O_WRONLY) | O_CREAT |
                    O_TRUNC;
                break;
        case 'a':
                flags = (mode[1] == '+' ? O_RDWR : O_WRONLY) | O_CREAT |
                    O_APPEND;
                break;
        default:
                log_fatal("monitor_fopen: bad call");
        }

        cur_umask = umask(0);
        (void)umask(cur_umask);
        mask = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
        mask &= ~cur_umask;

        fd = monitor_open(path, flags, mask);
        if (fd < 0)
                return NULL;

        /* Got the fd, attach a FILE * to it.  */
        fp = fdopen(fd, mode);
        if (!fp) {
                log_error("monitor_fopen: fdopen() failed");
                saved_errno = errno;
                close(fd);
                errno = saved_errno;
                return NULL;
        }
        return fp;
}

int
monitor_stat(const char *path, struct stat *sb)
{
        int     fd, r, saved_errno;

        /* O_NONBLOCK is needed for stat'ing fifos. */
        fd = monitor_open(path, O_RDONLY | O_NONBLOCK, 0);
        if (fd < 0)
                return -1;

        r = fstat(fd, sb);
        saved_errno = errno;
        close(fd);
        errno = saved_errno;
        return r;
}

int
monitor_setsockopt(int s, int level, int optname, const void *optval,
    socklen_t optlen)
{
        int     ret, err, cmd;

        cmd = MONITOR_SETSOCKOPT;
        must_write(&cmd, sizeof cmd);
        if (mm_send_fd(m_state.s, s)) {
                log_print("monitor_setsockopt: read/write error");
                return -1;
        }

        must_write(&level, sizeof level);
        must_write(&optname, sizeof optname);
        must_write(&optlen, sizeof optlen);
        must_write(optval, optlen);

        must_read(&err, sizeof err);
        must_read(&ret, sizeof ret);
        if (err != 0)
                errno = err;
        return ret;
}

int
monitor_bind(int s, const struct sockaddr *name, socklen_t namelen)
{
        int     ret, err, cmd;

        cmd = MONITOR_BIND;
        must_write(&cmd, sizeof cmd);
        if (mm_send_fd(m_state.s, s)) {
                log_print("monitor_bind: read/write error");
                return -1;
        }

        must_write(&namelen, sizeof namelen);
        must_write(name, namelen);

        must_read(&err, sizeof err);
        must_read(&ret, sizeof ret);
        if (err != 0)
                errno = err;
        return ret;
}

int
monitor_req_readdir(const char *filename)
{
        int cmd, err;
        size_t len;

        cmd = MONITOR_REQ_READDIR;
        must_write(&cmd, sizeof cmd);

        len = strlen(filename);
        must_write(&len, sizeof len);
        must_write(filename, len);

        must_read(&err, sizeof err);
        if (err == -1)
                must_read(&errno, sizeof errno);

        return err;
}

int
monitor_readdir(char *file, size_t size)
{
        int fd;
        size_t len;

        must_read(&len, sizeof len);
        if (len == 0)
                return -1;
        if (len >= size)
                log_fatal("monitor_readdir: received bad length from monitor");
        must_read(file, len);
        file[len] = '\0';
        fd = mm_receive_fd(m_state.s);
        return fd;
}

void
monitor_init_done(void)
{
        int     cmd;

        cmd = MONITOR_INIT_DONE;
        must_write(&cmd, sizeof cmd);
}

/*
 * Start of code running with privileges (the monitor process).
 */

static void
set_monitor_signals(void)
{
        int n;

        for (n = 1; n < _NSIG; n++)
                signal(n, SIG_DFL);

        /* Forward some signals to the child. */
        signal(SIGTERM, sig_pass_to_chld);
        signal(SIGHUP, sig_pass_to_chld);
        signal(SIGUSR1, sig_pass_to_chld);
}

static void
sig_pass_to_chld(int sig)
{
        int     oerrno = errno;

        if (m_state.pid > 0)
                kill(m_state.pid, sig);
        errno = oerrno;
}

/* This function is where the privileged process waits(loops) indefinitely.  */
void
monitor_loop(int debug)
{
        int      msgcode;

        if (!debug)
                log_to(0);

        for (;;) {
                must_read(&msgcode, sizeof msgcode);

                switch (msgcode) {
                case MONITOR_GET_FD:
                        m_priv_getfd();
                        break;

                case MONITOR_PFKEY_OPEN:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_PFKEY_OPEN"));
                        m_priv_pfkey_open();
                        break;

                case MONITOR_SETSOCKOPT:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_SETSOCKOPT"));
                        m_priv_setsockopt();
                        break;

                case MONITOR_BIND:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_BIND"));
                        m_priv_bind();
                        break;

                case MONITOR_REQ_READDIR:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_REQ_READDIR"));
                        m_priv_req_readdir();
                        break;

                case MONITOR_INIT_DONE:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_INIT_DONE"));
                        break;

                case MONITOR_SHUTDOWN:
                        LOG_DBG((LOG_MISC, 80,
                            "monitor_loop: MONITOR_SHUTDOWN"));
                        break;

                default:
                        log_print("monitor_loop: got unknown code %d",
                            msgcode);
                }
        }

        exit(0);
}


/* Privileged: called by monitor_loop.  */
static void
m_priv_pfkey_open(void)
{
        int     fd, err = 0;

        fd = pf_key_v2_open();
        if (fd < 0)
                err = -1;

        must_write(&err, sizeof err);

        if (fd > 0 && mm_send_fd(m_state.s, fd)) {
                log_error("m_priv_pfkey_open: read/write operation failed");
                close(fd);
                return;
        }
        close(fd);
}

/* Privileged: called by monitor_loop.  */
static void
m_priv_getfd(void)
{
        char    path[PATH_MAX];
        size_t  len;
        int     v, flags, ret;
        int     err = 0;
        mode_t  mode;

        must_read(&len, sizeof len);
        if (len == 0 || len >= sizeof path)
                log_fatal("m_priv_getfd: invalid pathname length");

        must_read(path, len);
        path[len] = '\0';
        if (strlen(path) != len)
                log_fatal("m_priv_getfd: invalid pathname");

        must_read(&flags, sizeof flags);
        must_read(&mode, sizeof mode);

        if ((ret = m_priv_local_sanitize_path(path, sizeof path, flags))
            != 0) {
                if (errno != ENOENT)
                        log_print("m_priv_getfd: illegal path \"%s\"", path);
                err = errno;
                v = -1;
        } else {
                if ((v = open(path, flags, mode)) == -1)
                        err = errno;
        }

        must_write(&err, sizeof err);

        if (v != -1) {
                if (mm_send_fd(m_state.s, v) == -1)
                        log_error("m_priv_getfd: sending fd failed");
                close(v);
        }
}

/* Privileged: called by monitor_loop.  */
static void
m_priv_setsockopt(void)
{
        int              sock, level, optname, v;
        int              err = 0;
        char            *optval = 0;
        socklen_t        optlen;

        sock = mm_receive_fd(m_state.s);
        if (sock < 0) {
                log_print("m_priv_setsockopt: read/write error");
                return;
        }

        must_read(&level, sizeof level);
        must_read(&optname, sizeof optname);
        must_read(&optlen, sizeof optlen);

        optval = malloc(optlen);
        if (!optval) {
                log_print("m_priv_setsockopt: malloc failed");
                close(sock);
                return;
        }

        must_read(optval, optlen);

        if (m_priv_check_sockopt(level, optname) != 0) {
                err = EACCES;
                v = -1;
        } else {
                v = setsockopt(sock, level, optname, optval, optlen);
                if (v < 0)
                        err = errno;
        }

        close(sock);
        sock = -1;

        must_write(&err, sizeof err);
        must_write(&v, sizeof v);

        free(optval);
        return;
}

/* Privileged: called by monitor_loop.  */
static void
m_priv_bind(void)
{
        int              sock, v, err = 0;
        struct sockaddr *name = 0;
        socklen_t        namelen;

        sock = mm_receive_fd(m_state.s);
        if (sock < 0) {
                log_print("m_priv_bind: read/write error");
                return;
        }

        must_read(&namelen, sizeof namelen);
        name = malloc(namelen);
        if (!name) {
                log_print("m_priv_bind: malloc failed");
                close(sock);
                return;
        }
        must_read((char *)name, namelen);

        if (m_priv_check_bind(name, namelen) != 0) {
                err = EACCES;
                v = -1;
        } else {
                v = bind(sock, name, namelen);
                if (v == -1) {
                        log_error("m_priv_bind: bind(%d,%p,%d) returned %d",
                            sock, name, namelen, v);
                        err = errno;
                }
        }

        close(sock);
        sock = -1;

        must_write(&err, sizeof err);
        must_write(&v, sizeof v);

        free(name);
        return;
}

/*
 * Help functions, used by both privileged and unprivileged code
 */

/*
 * Read data with the assertion that it all must come through, or else abort
 * the process.  Based on atomicio() from openssh.
 */
static void
must_read(void *buf, size_t n)
{
        char *s = buf;
        size_t pos = 0;
        ssize_t res;

        while (n > pos) {
                res = read(m_state.s, s + pos, n - pos);
                switch (res) {
                case -1:
                        if (errno == EINTR || errno == EAGAIN)
                                continue;
                case 0:
                        monitor_exit(0);
                default:
                        pos += res;
                }
        }
}

/*
 * Write data with the assertion that it all has to be written, or else abort
 * the process.  Based on atomicio() from openssh.
 */
static void
must_write(const void *buf, size_t n)
{
        const char *s = buf;
        size_t pos = 0;
        ssize_t res;

        while (n > pos) {
                res = write(m_state.s, s + pos, n - pos);
                switch (res) {
                case -1:
                        if (errno == EINTR || errno == EAGAIN)
                                continue;
                case 0:
                        monitor_exit(0);
                default:
                        pos += res;
                }
        }
}

/* Check that path/mode is permitted.  */
static int
m_priv_local_sanitize_path(const char *path, size_t pmax, int flags)
{
        char new_path[PATH_MAX], var_run[PATH_MAX], *enddir;

        /*
         * We only permit paths starting with
         *  /etc/isakmpd/       (read only)
         *  /var/run/           (rw)
         */

        if (realpath(path, new_path) == NULL) {
                if (errno != ENOENT)
                        return 1;
                /*
                 * It is ok if the directory exists,
                 * but the file should be created.
                 */
                if (strlcpy(new_path, path, sizeof(new_path)) >=
                    sizeof(new_path))
                        return 1;
                enddir = strrchr(new_path, '/');
                if (enddir == NULL || enddir[1] == '\0')
                        return 1;
                enddir[1] = '\0';
                if (realpath(new_path, new_path) == NULL) {
                        errno = ENOENT;
                        return 1;
                }
                enddir = strrchr(path, '/');
                strlcat(new_path, enddir, sizeof(new_path));
        }

        if (realpath("/var/run/", var_run) == NULL)
                return 1;
        strlcat(var_run, "/", sizeof(var_run));

        if (strncmp(var_run, new_path, strlen(var_run)) == 0)
                return 0;

        if (strncmp(ISAKMPD_ROOT, new_path, strlen(ISAKMPD_ROOT)) == 0 &&
            (flags & O_ACCMODE) == O_RDONLY)
                return 0;

        errno = EACCES;
        return 1;
}

/* Check setsockopt */
static int
m_priv_check_sockopt(int level, int name)
{
        switch (level) {
                /* These are allowed */
                case SOL_SOCKET:
                case IPPROTO_IP:
                case IPPROTO_IPV6:
                break;

        default:
                log_print("m_priv_check_sockopt: Illegal level %d", level);
                return 1;
        }

        switch (name) {
                /* These are allowed */
        case SO_REUSEPORT:
        case SO_REUSEADDR:
        case IP_AUTH_LEVEL:
        case IP_ESP_TRANS_LEVEL:
        case IP_ESP_NETWORK_LEVEL:
        case IP_IPCOMP_LEVEL:
        case IPV6_AUTH_LEVEL:
        case IPV6_ESP_TRANS_LEVEL:
        case IPV6_ESP_NETWORK_LEVEL:
        case IPV6_IPCOMP_LEVEL:
                break;

        default:
                log_print("m_priv_check_sockopt: Illegal option name %d",
                    name);
                return 1;
        }

        return 0;
}

/* Check bind */
static int
m_priv_check_bind(const struct sockaddr *sa, socklen_t salen)
{
        in_port_t       port;

        if (sa == NULL) {
                log_print("NULL address");
                return 1;
        }
        if (SA_LEN(sa) != salen) {
                log_print("Length mismatch: %lu %lu", (unsigned long)sa->sa_len,
                    (unsigned long)salen);
                return 1;
        }
        switch (sa->sa_family) {
        case AF_INET:
                if (salen != sizeof(struct sockaddr_in)) {
                        log_print("Invalid inet address length");
                        return 1;
                }
                port = ((const struct sockaddr_in *)sa)->sin_port;
                break;
        case AF_INET6:
                if (salen != sizeof(struct sockaddr_in6)) {
                        log_print("Invalid inet6 address length");
                        return 1;
                }
                port = ((const struct sockaddr_in6 *)sa)->sin6_port;
                break;
        default:
                log_print("Unknown address family");
                return 1;
        }

        port = ntohs(port);

        if (port != ISAKMP_PORT_DEFAULT && port < 1024) {
                log_print("Disallowed port %u", port);
                return 1;
        }
        return 0;
}

static void
m_priv_req_readdir(void)
{
        size_t len;
        char path[PATH_MAX];
        DIR *dp;
        struct dirent *file;
        struct stat sb;
        int off, size, fd, ret, serrno;

        must_read(&len, sizeof len);
        if (len == 0 || len >= sizeof path)
                log_fatal("m_priv_req_readdir: invalid pathname length");
        must_read(path, len);
        path[len] = '\0';
        if (strlen(path) != len)
                log_fatal("m_priv_req_readdir: invalid pathname");

        off = strlen(path);
        size = sizeof path - off;

        if ((dp = opendir(path)) == NULL) {
                serrno = errno;
                ret = -1;
                must_write(&ret, sizeof ret);
                must_write(&serrno, sizeof serrno);
                return;
        }

        /* report opendir() success */
        ret = 0;
        must_write(&ret, sizeof ret);

        while ((file = readdir(dp)) != NULL) {
                strlcpy(path + off, file->d_name, size);

                if (m_priv_local_sanitize_path(path, sizeof path, O_RDONLY)
                    != 0)
                        continue;
                fd = open(path, O_RDONLY);
                if (fd == -1) {
                        log_error("m_priv_req_readdir: open "
                            "(\"%s\", O_RDONLY, 0) failed", path);
                        continue;
                }
                if ((fstat(fd, &sb) == -1) ||
                    !(S_ISREG(sb.st_mode) || S_ISLNK(sb.st_mode))) {
                        close(fd);
                        continue;
                }

                len = strlen(path);
                must_write(&len, sizeof len);
                must_write(path, len);

                mm_send_fd(m_state.s, fd);
                close(fd);
        }
        closedir(dp);

        len = 0;
        must_write(&len, sizeof len);
}