root/usr.sbin/faithd/tcp.c
/*      $NetBSD: tcp.c,v 1.11 2011/08/30 21:14:06 joerg Exp $   */
/*      $KAME: tcp.c,v 1.10 2002/08/20 23:01:01 itojun Exp $    */

/*
 * Copyright (C) 1997 and 1998 WIDE Project.
 * 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.
 * 3. Neither the name of the project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE PROJECT 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 PROJECT 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/param.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/wait.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>

#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#include "faithd.h"

static char tcpbuf[16*1024];
        /* bigger than MSS and may be lesser than window size */
static ssize_t tblen, tboff;
static int oob_exists;
static fd_set readfds, writefds, exceptfds;
static char atmark_buf[2];
static pid_t cpid = (pid_t)0;
static pid_t ppid = (pid_t)0;
volatile time_t child_lastactive = (time_t)0;
static time_t parent_lastactive = (time_t)0;

static void sig_ctimeout(int);
static void sig_child(int) __dead;
static void notify_inactive(void);
static void notify_active(void);
static void send_data(int, int, const char *, int);
static void relay(int, int, const char *, int) __dead;

/*
 * Inactivity timer:
 * - child side (ppid != 0) will send SIGUSR1 to parent every (FAITH_TIMEOUT/4)
 *   second if traffic is active.  if traffic is inactive, don't send SIGUSR1.
 * - parent side (ppid == 0) will check the last SIGUSR1 it have seen.
 */
static void
/*ARGSUSED*/
sig_ctimeout(int sig)
{
        /* parent side: record notification from the child */
        if (dflag)
                syslog(LOG_DEBUG, "activity timer from child");
        child_lastactive = time(NULL);
}

/* parent will terminate if child dies. */
static void
/*ARGSUSED*/
sig_child(int sig)
{
        int status;
        pid_t pid;

        pid = wait3(&status, WNOHANG, (struct rusage *)0);
        if (pid > 0 && WEXITSTATUS(status))
                syslog(LOG_WARNING, "child %ld exit status 0x%x",
                    (long)pid, status);
        exit_success("terminate connection due to child termination");
}

static void
notify_inactive()
{
        time_t t;

        /* only on parent side... */
        if (ppid)
                return;

        /* parent side should check for timeout. */
        t = time(NULL);
        if (dflag) {
                syslog(LOG_DEBUG, "parent side %sactive, child side %sactive",
                        (FAITH_TIMEOUT < t - parent_lastactive) ? "in" : "",
                        (FAITH_TIMEOUT < t - child_lastactive) ? "in" : "");
        }

        if (FAITH_TIMEOUT < t - child_lastactive
         && FAITH_TIMEOUT < t - parent_lastactive) {
                /* both side timeouted */
                (void)signal(SIGCHLD, SIG_DFL);
                (void)kill(cpid, SIGTERM);
                (void)wait(NULL);
                exit_failure("connection timeout");
                /* NOTREACHED */
        }
}

static void
notify_active()
{
        if (ppid) {
                /* child side: notify parent of active traffic */
                time_t t;
                t = time(NULL);
                if (FAITH_TIMEOUT / 4 < t - child_lastactive) {
                        if (kill(ppid, SIGUSR1) < 0) {
                                exit_failure("terminate connection due to parent termination");
                                /* NOTREACHED */
                        }
                        child_lastactive = t;
                }
        } else {
                /* parent side */
                parent_lastactive = time(NULL);
        }
}

static void
/*ARGSUSED*/
send_data(int s_rcv, int s_snd, const char *service, int direction)
{
        ssize_t cc;

        if (oob_exists) {
                cc = send(s_snd, atmark_buf, 1, MSG_OOB);
                if (cc == -1)
                        goto retry_or_err;
                oob_exists = 0;
                if (s_rcv >= FD_SETSIZE)
                        exit_failure("descriptor too big");
                FD_SET(s_rcv, &exceptfds);
        }

        for (; tboff < tblen; tboff += cc) {
                cc = write(s_snd, tcpbuf + tboff, (size_t)(tblen - tboff));
                if (cc < 0)
                        goto retry_or_err;
        }
#ifdef DEBUG
        if (tblen) {
                if (tblen >= sizeof(tcpbuf))
                        tblen = sizeof(tcpbuf) - 1;
                tcpbuf[tblen] = '\0';
                syslog(LOG_DEBUG, "from %s (%dbytes): %s",
                       direction == 1 ? "client" : "server", tblen, tcpbuf);
        }
#endif /* DEBUG */
        tblen = 0; tboff = 0;
        if (s_snd >= FD_SETSIZE)
                exit_failure("descriptor too big");
        FD_CLR(s_snd, &writefds);
        if (s_rcv >= FD_SETSIZE)
                exit_failure("descriptor too big");
        FD_SET(s_rcv, &readfds);
        return;
    retry_or_err:
        if (errno != EAGAIN)
                exit_failure("writing relay data failed: %s", strerror(errno));
        if (s_snd >= FD_SETSIZE)
                exit_failure("descriptor too big");
        FD_SET(s_snd, &writefds);
}

static void
relay(int s_rcv, int s_snd, const char *service, int direction)
{
        int atmark, error, maxfd;
        struct timeval tv;
        fd_set oreadfds, owritefds, oexceptfds;

        FD_ZERO(&readfds);
        FD_ZERO(&writefds);
        FD_ZERO(&exceptfds);
        (void)fcntl(s_snd, F_SETFD, O_NONBLOCK);
        oreadfds = readfds; owritefds = writefds; oexceptfds = exceptfds;
        if (s_rcv >= FD_SETSIZE)
                exit_failure("descriptor too big");
        FD_SET(s_rcv, &readfds);
        FD_SET(s_rcv, &exceptfds);
        oob_exists = 0;
        maxfd = (s_rcv > s_snd) ? s_rcv : s_snd;

        for (;;) {
                tv.tv_sec = FAITH_TIMEOUT / 4;
                tv.tv_usec = 0;
                oreadfds = readfds;
                owritefds = writefds;
                oexceptfds = exceptfds;
                error = select(maxfd + 1, &readfds, &writefds, &exceptfds, &tv);
                if (error == -1) {
                        if (errno == EINTR)
                                continue;
                        exit_failure("select: %s", strerror(errno));
                } else if (error == 0) {
                        readfds = oreadfds;
                        writefds = owritefds;
                        exceptfds = oexceptfds;
                        notify_inactive();
                        continue;
                }

                /* activity notification */
                notify_active();

                if (FD_ISSET(s_rcv, &exceptfds)) {
                        error = ioctl(s_rcv, SIOCATMARK, &atmark);
                        if (error != -1 && atmark == 1) {
                                ssize_t cc;
oob_read_retry:
                                cc = read(s_rcv, atmark_buf, 1);
                                if (cc == 1) {
                                        if (s_rcv >= FD_SETSIZE)
                                                exit_failure("descriptor too big");
                                        FD_CLR(s_rcv, &exceptfds);
                                        if (s_snd >= FD_SETSIZE)
                                                exit_failure("descriptor too big");
                                        FD_SET(s_snd, &writefds);
                                        oob_exists = 1;
                                } else if (cc == -1) {
                                        if (errno == EINTR)
                                                goto oob_read_retry;
                                        exit_failure("reading oob data failed"
                                                     ": %s",
                                                     strerror(errno));
                                }
                        }
                }
                if (FD_ISSET(s_rcv, &readfds)) {
                    relaydata_read_retry:
                        tblen = read(s_rcv, tcpbuf, sizeof(tcpbuf));
                        tboff = 0;

                        switch (tblen) {
                        case -1:
                                if (errno == EINTR)
                                        goto relaydata_read_retry;
                                exit_failure("reading relay data failed: %s",
                                             strerror(errno));
                                /* NOTREACHED */
                        case 0:
                                /* to close opposite-direction relay process */
                                (void)shutdown(s_snd, 0);

                                (void)close(s_rcv);
                                (void)close(s_snd);
                                exit_success("terminating %s relay", service);
                                /* NOTREACHED */
                        default:
                                if (s_rcv >= FD_SETSIZE)
                                        exit_failure("descriptor too big");
                                FD_CLR(s_rcv, &readfds);
                                if (s_snd >= FD_SETSIZE)
                                        exit_failure("descriptor too big");
                                FD_SET(s_snd, &writefds);
                                break;
                        }
                }
                if (FD_ISSET(s_snd, &writefds))
                        send_data(s_rcv, s_snd, service, direction);
        }
}

void
tcp_relay(int s_src, int s_dst, const char *service)
{
        syslog(LOG_INFO, "starting %s relay", service);

        child_lastactive = parent_lastactive = time(NULL);

        cpid = fork();
        switch (cpid) {
        case -1:
                exit_failure("tcp_relay: can't fork grand child: %s",
                    strerror(errno));
                /* NOTREACHED */
        case 0:
                /* child process: relay going traffic */
                ppid = getppid();
                /* this is child so reopen log */
                closelog();
                openlog(logname, LOG_PID | LOG_NOWAIT, LOG_DAEMON);
                relay(s_src, s_dst, service, 1);
                /* NOTREACHED */
        default:
                /* parent process: relay coming traffic */
                ppid = (pid_t)0;
                (void)signal(SIGUSR1, sig_ctimeout);
                (void)signal(SIGCHLD, sig_child);
                relay(s_dst, s_src, service, 0);
                /* NOTREACHED */
        }
}