//-----------------------------------------------------------------------------
// Copyright © 2005 - Philip Howard - All rights reserved
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//-----------------------------------------------------------------------------
// author	Philip Howard
// email	libh at ipal dot org
// homepage	http://phil.ipal.org/
//-----------------------------------------------------------------------------
// This file is best viewed using a fixed spaced font such as Courier
// and in a display at least 120 columns wide.
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// program	tcprelay
//
// purpose	Accept connections and relay them through to a specified
//		network address and port.  Keep a capture log of the TCP
//		traffic if specified.
//
// syntax	tcprelay  [options]  listenport  connecthost  connectport
//
// options	-a <file>	access list filename
//		-b <size>	specify buffer size
//		-c		enable capture (in changed directory)
//		-d <dir>	change directory
//		-h		show help
//		-l <file>	log file
//              -p <file>	store master process ID here
//		-q		be quiet with startup messages
//		-r <size>	specify buffer size for receive
//		-s <size>	specify buffer size for send
//		-u <user>	switch user after binding socket
//		-v		be verbose with startup messages
//		-V		show version number
//
// note		The chroot() system call will be made to change the root to
//		the current directory after all directory changes are done.
//		Combined with changing to a safe directory and switching to
//		a non-root user, this offers some protection against possible
//		exploits, few as they may be given the simplicity of this.
//
// requires	LIBH
//
// compile	gcc -O2 -o tcprelay tcprelay.c -lh
//-----------------------------------------------------------------------------
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <netdb.h>
#include <netinet/in.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>


#define _GNU_SOURCE
#include <getopt.h>
extern char *optarg;
extern int optind, opterr, optopt;

#include <libh/daemon.h>
#include <libh/io.h>
#include <libh/net.h>
#include <libh/string.h>
#include <libh/time.h>
#include <libh/vrb.h>

extern int h_errno;


//-----------------------------------------------------------------------------
// Globally shared variables.
//-----------------------------------------------------------------------------
static char *		program_name	;


//-----------------------------------------------------------------------------
// function	show_help
//-----------------------------------------------------------------------------
int
show_help ()
{
    fprintf( stderr,
	     "\n"
	     "syntax:  %s  [options]  listenport  serverhost  serverport\n"
	     "\n"
	     "options: -a  <file>       access list filename\n"
	     "         -b  <size>       specify buffer size\n"
	     "         -c               enable traffic capture to a file\n"
	     "         -d  <directory>  change directory for files\n"
	     "         -g  <group>      set effective group for files\n"
	     "         -l  <file>       log file\n"
	     "         -h               show this help\n"
	     "         -p  <file>       store master process ID here\n"
	     "         -q               be quite with start messages\n"
	     "         -r  <size>       specify buffer size for receive\n"
	     "         -s  <size>       specify buffer size for send\n"
	     "         -u  <user>       set effective user for files\n"
	     "         -v               be verbose with start messages\n"
	     "         -V               show version number\n"
	     "\n"
	     "signals: HUP              graceful restart (wait for workers)\n"
	     "         USR1             fast restart (kill workers)\n"
	     "         USR2             graceful shutdown (wait for workers)\n"
	     "         INT,TERM,QUIT    fast shutdown (kill workers)\n"
	     "\n",
	     program_name );
    return 1;
}

//-----------------------------------------------------------------------------
// function	write_capture_data
//
// purpose	Write data to a capture file.
//
// arguments	1 (int) file descriptor
//		2 (int) data type character
//		3 (const char *) data to write
//		4 (size_t) length of data to write
//
// returns	(void)
//
// format	Byte order is big endian.
//		bytes 0-3:	number of seconds since epoch
//		bytes 4-6:	number of microseconds more
//		byte 7:		data chunk type code
//		bytes 8-9:	length of data following header
//-----------------------------------------------------------------------------
static inline
void
write_capture_data (
    int			arg_fd
    ,
    int			arg_type
    ,
    const char *	arg_ptr
    ,
    size_t		arg_len
    )
{
    if ( arg_fd < 0 ) return;

    {
	long long	this_time		;
	char *		ptr			;
	size_t		len			;
	char		header_mem	[10]	;

	this_time = current_timeus();
	ptr = header_mem;
	*    ptr = this_time >> 48;
	* ++ ptr = this_time >> 40;
	* ++ ptr = this_time >> 32;
	* ++ ptr = this_time >> 24;
	* ++ ptr = this_time >> 16;
	* ++ ptr = this_time >>  8;
	* ++ ptr = this_time;
	* ++ ptr = arg_type;
	* ++ ptr = arg_len >> 8;
	* ++ ptr = arg_len;

	len = 10;
	ptr = header_mem;
	while ( len ) {
	    ssize_t write_len;
	    if ( ( write_len = write( arg_fd, ptr, len ) ) < 0 ) return;
	    ptr += write_len;
	    len -= write_len;
	}
    }

    {
	const char *ptr;
	size_t len;

	len = arg_len;
	ptr = arg_ptr;
	while ( len ) {
	    ssize_t write_len;
	    if ( ( write_len = write( arg_fd, ptr, len ) ) < 0 ) return;
	    ptr += write_len;
	    len -= write_len;
	}
    }

    return;
}


//-----------------------------------------------------------------------------
// Main program.
//-----------------------------------------------------------------------------
int
main (
    int		argc
    ,
    char * *	argv
    )
{
    static const struct option	long_opts	[]	= {
	{ "accessfile",	required_argument,	NULL,	'a' },
	{ "access",	required_argument,	NULL,	'a' },
	{ "buffersize",	required_argument,	NULL,	'b' },
	{ "cap",	no_argument,		NULL,	'c' },
	{ "capture",	no_argument,		NULL,	'c' },
	{ "dir",	required_argument,	NULL,	'd' },
	{ "directory",	required_argument,	NULL,	'd' },
	{ "chdir",	required_argument,	NULL,	'd' },
	{ "chroot",	required_argument,	NULL,	'd' },
	{ "gid",	required_argument,	NULL,	'g' },
	{ "group",	required_argument,	NULL,	'g' },
	{ "help",	no_argument,		NULL,	'h' },
	{ "logfile",	required_argument,	NULL,	'l' },
	{ "log",	required_argument,	NULL,	'l' },
	{ "pidfile",	required_argument,	NULL,	'p' },
	{ "quiet",	no_argument,		NULL,	'q' },
	{ "recvsize",	required_argument,	NULL,	'r' },
	{ "sendsize",	required_argument,	NULL,	's' },
	{ "sizerecv",	required_argument,	NULL,	'r' },
	{ "sizesend",	required_argument,	NULL,	's' },
	{ "uid",	required_argument,	NULL,	'u' },
	{ "user",	required_argument,	NULL,	'u' },
	{ "verbose",	no_argument,		NULL,	'v' },
	{ "version",	no_argument,		NULL,	'V' },
	{ NULL,		0,			NULL,	0 }
    };

    static const int		num1			= 1;

    static const char		short_opts	[]	= ":a:b:cd:g:hl:p:qr:s:u:vV";


    struct pollfd	poll_list	[3]	;

    struct sockaddr_in	listen_addr		;
    struct sockaddr_in	client_addr		;
    struct sockaddr_in	server_addr		;
    struct sockaddr_in	local_addr		;

    socklen_t		client_addrlen		;

    struct hostent *	hostent_p		;

    vrb_p		client_to_server_vrb	;
    vrb_p		server_to_client_vrb	;

    char * *		server_addr_p		;

    const char *	dir_name		;
    char *		listen_port_str		;
    char *		server_host_str		;
    char *		server_port_str		;
    char *		opt_accessfile		;
    char *		opt_group		;
    char *		opt_user		;
    char *		opt_pidfile		;
    char *		opt_end			;

    long		start_count		;

    size_t		opt_buffersize		;
    size_t		opt_buffersize_send	;
    size_t		opt_buffersize_recv	;

    int			server_to_client_eof	;
    int			recv_end		;
    int			client_to_server_eof	;
    int			send_end		;
    int			access_fd		;
    int			listen_fd		;
    int			client_fd		;
    int			server_fd		;
    int			capture_fd		;
    int			server_port		;
    int			listen_port		;
    int			master_pid		;
    int			opt_help		;
    int			opt_capture		;
    int			opt_verbose		;
    int			opt_version		;
    int			error_count		;
    int			error_chdir		;

    gid_t		opt_gid			;
    gid_t		this_gid		;
    uid_t		opt_uid			;
    uid_t		this_uid		;

    char		dir_str		[PATH_MAX+8]	;
    char		path_str	[PATH_MAX+8]	;



    //----------------------
    // Initialize variables.
    //----------------------
    error_count = 0;
    error_chdir = 0;

    opt_buffersize = 0;
    opt_buffersize_send = 0;
    opt_buffersize_recv = 0;

    opt_accessfile = NULL;
    opt_group = NULL;
    opt_user = NULL;
    opt_pidfile = NULL;

    opt_help = 0;
    opt_capture = 0;
    opt_verbose = -1;

    opt_gid = -1;
    opt_uid = -1;

    capture_fd = -1;

    dir_name = ".";

    this_uid = getuid();
    this_gid = getgid();


    //--------------------------------------------------------
    // At this point main() is running in the startup process.
    // This is the process that the shell is waiting to exit.
    //--------------------------------------------------------
    program_name = str_tail_one( argv[0], '/' );
    program_name = strdup( program_name );
    daemon_set_program_name( program_name );

    //--------------
    // Scan options.
    //--------------
    for (;;) {
	int opt;

	opt = getopt_long( argc, argv, short_opts, long_opts, NULL );
	if ( opt < 0 ) break;

	switch ( opt ) {

	case 'a':
	    opt_accessfile = optarg;
	    continue;

	case 'b':
	    opt_buffersize = str_metric_to_ul( optarg, & opt_end, 0, 1024, 1024 );
	    continue;

	case 'c':
	    opt_capture = 1;
	    continue;

	case 'd':
	    if ( error_chdir == 0 ) {
		if ( chdir( optarg ) < 0 ) {
		    fprintf( stderr, "%s: chdir(\"%s\"): %s\n", program_name, optarg, strerror( errno ) );
		    ++ error_count;
		    ++ error_chdir;
		}
	    }
	    else if ( error_chdir == 1 ) {
		fprintf( stderr, "%s: no further messages will be issued for -d options", program_name );
		++ error_chdir;
	    }
	    continue;

	case 'g':
	    opt_gid = strtoul( optarg, & opt_end, 0 );
	    if ( * opt_end ) {
		struct group * this_group;
		opt_group = optarg;
		this_group = getgrnam( opt_group );
		if ( ! this_group ) {
		    fprintf( stderr, "%s: group \"%s\" not found\n", program_name, opt_group );
		    ++ error_count;
		    continue;
		}
		opt_gid = this_group->gr_gid;
	    }
	    continue;

	case 'h':
	    ++ opt_help;
	    continue;

	case 'p':
	    opt_pidfile = optarg;
	    continue;

	case 'q':
	    opt_verbose = 0;
	    continue;

	case 'r':
	    opt_buffersize_recv = str_metric_to_ul( optarg, & opt_end, 0, 1024, 1024 );
	    continue;

	case 's':
	    opt_buffersize_send = str_metric_to_ul( optarg, & opt_end, 0, 1024, 1024 );
	    continue;

	case 'u':
	    opt_uid = strtoul( optarg, & opt_end, 0 );
	    if ( * opt_end ) {
		struct passwd * this_user;
		opt_user = optarg;
		this_user = getpwnam( opt_user );
		if ( ! this_user ) {
		    fprintf( stderr, "%s: user \"%s\" not found\n", program_name, opt_user );
		    ++ error_count;
		    continue;
		}
		opt_uid = this_user->pw_uid;
	    }
	    continue;

	case 'v':
	    opt_verbose = 1;
	    continue;

	case 'V':
	    ++ opt_version;
	    continue;

	default:
	    if ( opt ) {
		fprintf( stderr, "%s: unrecognized option '%c'\n", program_name, opt );
	    } else {
		fprintf( stderr, "%s: unrecognized option \"%s\"\n", program_name, argv[optind] );
	    }
	    ++ error_count;
	    continue;
	}
    }

    //----------------------------------
    // Quit now if there are any errors.
    //----------------------------------
    if ( error_count ) return 1;

    //------------------------------
    // Three arguments are required.
    //------------------------------
    if ( argc - optind < 3 ) return show_help();
    listen_port_str = argv[ optind + 0 ];
    server_host_str = argv[ optind + 1 ];
    server_port_str = argv[ optind + 2 ];

    //---------------------
    // Adjust buffer sizes.
    //---------------------
    if ( opt_buffersize_send == 0 ) opt_buffersize_send = opt_buffersize;
    if ( opt_buffersize_send == 0 ) opt_buffersize_send = 65536;
    opt_buffersize_send -= 1U;
    opt_buffersize_send |= 0x00000fffUL;
    opt_buffersize_send += 1U;

    if ( opt_buffersize_recv == 0 ) opt_buffersize_recv = opt_buffersize;
    if ( opt_buffersize_recv == 0 ) opt_buffersize_recv = 65536;
    opt_buffersize_recv -= 1U;
    opt_buffersize_recv |= 0x00000fffUL;
    opt_buffersize_recv += 1U;

    //--------------------------------------------
    // Convert or look up listen port to a number.
    //--------------------------------------------
    listen_port = get_port_by_name( listen_port_str, "tcp" );
    if ( listen_port < 0 ) {
	fprintf( stderr, "%s: invalid listen port \"%s\"\n", program_name, listen_port_str );
	return 1;
    }

    //--------------------------------------------
    // Convert or look up server port to a number.
    //--------------------------------------------
    server_port = get_port_by_name( server_port_str, "tcp" );
    if ( server_port < 0 ) {
	fprintf( stderr, "%s: invalid server port \"%s\"\n", program_name, server_port_str );
	return 1;
    }

    //----------------------------------------------
    // Look up the specified server host address to
    // be sure it exists before entering background.
    //----------------------------------------------
    hostent_p = gethostbyname( server_host_str );
    if ( ! hostent_p ) {
	fprintf( stderr, "%s: gethostbyname(\"%s\"): %s\n",
		 program_name, server_host_str, hstrerror( h_errno ) );
	return 1;
    }

    //-----------------------------------------------
    // Switch from startup process to master process.
    //-----------------------------------------------
    daemon_set_start_timeout( 30 );
    if ( daemon_start() < 0 ) _exit( 1 );
    argv[0][0] = 'M';
    master_pid = getpid();

    //------------------------------------------------------------------
    // Initialize a local address structure for outgoing socket binding.
    //------------------------------------------------------------------
    local_addr = hbo_to_sockaddr_in( 0, 0 );
//    local_addr.sin_family = AF_INET;
//    local_addr.sin_port = 0;
//    memset( & local_addr.sin_addr, 0, sizeof (struct in_addr) );

    //--------------------------
    // Set up the listen socket.
    //--------------------------
    listen_fd = socket( PF_INET, SOCK_STREAM, IPPROTO_TCP );
    if ( listen_fd < 0 ) {
	fprintf( stderr, "%s: socket(PF_INET,SOCK_STREAM,IPPROTO_TCP): %s\n",
		 program_name, strerror( errno ) );
	_exit( 1 );
    }
    if ( setsockopt( listen_fd, SOL_SOCKET, SO_REUSEADDR, (void*) & num1, (socklen_t) sizeof num1 ) < 0 ) {
	fprintf( stderr, "%s: setsockopt(,SOL_SOCKET,SO_REUSEADDR,,): %s\n",
		 program_name, strerror( errno ) );
	_exit( 1 );
    }
    listen_addr = hbo_to_sockaddr_in( 0, listen_port );
//    listen_addr.sin_family = AF_INET;
//    listen_addr.sin_port = htons( (uint16_t) listen_port );
//    listen_addr.sin_addr.s_addr = 0;
    if ( bind( listen_fd, (struct sockaddr *) & listen_addr, (socklen_t) sizeof listen_addr ) < 0 ) {
	if ( errno == EADDRINUSE ) { // Output a more meaningful error message.
	    fprintf( stderr, "%s: bind: Port %u already in use\n",
		     program_name, listen_port );
	} else {
	    fprintf( stderr, "%s: bind port %u: %s\n",
		     program_name, listen_port, strerror( errno ) );
	}
	_exit( 1 );
    }
    if ( listen( listen_fd, 128 ) < 0 ) {
	fprintf( stderr, "%s: listen(%d,128): %s\n",
		 program_name, listen_fd, strerror( errno ) );
	_exit( 1 );
    }

    //-------------------------------------
    // Write master process ID to pid file.
    //-------------------------------------
    if ( opt_pidfile ) {
	FILE * pidfile;
	char pidpath[ PATH_MAX + 1 ];
	snprintf( pidpath, sizeof pidpath, "%s.new", opt_pidfile );
	pidfile = fopen( pidpath, "w" );
	if ( pidfile ) {
	    fprintf( pidfile, "%u\n", master_pid );
	    fclose( pidfile );
	    rename( pidpath, opt_pidfile );
	}
    }

    //--------------------------------------------------------------
    // Note what directory we are now in before chroot() hides this.
    //--------------------------------------------------------------
    getcwd( dir_str, sizeof dir_str );

    //--------------------------------------------------------------------
    // Try to change this process root directory to the current directory.
    // This will fail if not running as root, but ignore that error.
    //--------------------------------------------------------------------
    if ( chroot( "." ) < 0 && geteuid() == 0 ) {
	fprintf( stderr, "%s: chroot( \".\" ): %s\n",
		 program_name, strerror( errno ) );
    }

    //---------------------------------------
    // Switch user and/or group as specified.
    //---------------------------------------
    if ( opt_gid >= 0 && setregid( opt_gid, opt_gid ) < 0 ) {
	fprintf( stderr, "%s: unable to switch to group id %u (specified as \"%s\"): %s\n",
		 program_name, opt_gid, opt_group, strerror( errno ) );
	++ error_count;
    }
    if ( opt_uid >= 0 && setreuid( opt_uid, opt_uid ) < 0 ) {
	fprintf( stderr, "%s: unable to switch to user id %u (specified as \"%s\"): %s\n",
		 program_name, opt_uid, opt_user, strerror( errno ) );
	++ error_count;
    }
    if ( error_count ) {
	fflush( NULL );
	_exit( 1 );
    }

    //----------------------------------------------
    // Switch from master process to server process.
    //----------------------------------------------
    daemon_set_restart_count_max( 256 );
    if ( daemon_master( & start_count ) != 0 ) {
	fprintf( stderr, "%s: server restart limit (%lu) exceeded\n", program_name, start_count );
	fflush( NULL );
	_exit( 1 );
    }
    argv[0][0] = 'S';

    //-----------------------------------
    // Need to launch a log process here.
    //-----------------------------------

    //-----------------------------------------------
    // If access list file is specified, open it now.
    //-----------------------------------------------
    if ( opt_accessfile ) {
	access_fd = open( opt_accessfile, O_RDONLY );
	if ( access_fd < 0 ) {
	    fprintf( stderr,
		     "%s: unable to open access list file: %s\n",
		     program_name, strerror( errno ) );
	    fflush( NULL );
	    _exit( 1 );
	}
    }

    //------------------------------------------------------------------------
    // If this is the first server start, output a message and signal success.
    //------------------------------------------------------------------------
    if ( start_count == 0 ) {
	int sep_ch;
	sep_ch = 0;
	if ( opt_verbose ) {
	    printf( "%s[%u]: port %s", program_name, master_pid, listen_port_str );
	    if ( listen_port_str[0] < '0' || '9' < listen_port_str[0] ) {
		printf( "(%u)", listen_port );
	    }
	    printf( " relaying to %s", server_host_str );
	    if ( server_host_str[0] < '0' || '9' < server_host_str[0] ) {
		fputc( '(', stdout );
		for ( server_addr_p = hostent_p->h_addr_list; * server_addr_p; ++ server_addr_p ) {
		    inet_ntop( hostent_p->h_addrtype, * server_addr_p, path_str, sizeof path_str );
		    if ( sep_ch ) fputc( sep_ch, stdout );
		    fputs( path_str, stdout );
		    sep_ch = ' ';
		}
		fputc( ')', stdout );
	    }
	    printf( " port %s", server_port_str );
	    if ( server_port_str[0] < '0' || '9' < server_port_str[0] ) {
		printf( "(%u)", server_port );
	    }
	    fputc( '\n', stdout );

	    if ( opt_capture ) {
		printf( "%s[%u]: port %d logging in directory \"%s\"\n",
			program_name, master_pid, listen_port, dir_str );
	    }

	    fflush( stdout );
	}
	daemon_success();
    }

    //-----------------------------------------------------------------
    // Read and parse access list file, and build address lookup table.
    //-----------------------------------------------------------------

    //-------------------------------------------------------------------
    // Run server loop which starts a worker process for each connection.
    //-------------------------------------------------------------------
    daemon_set_worker_count_max( 40 );
    {
	int fd_list[2];
	fd_list[0] = listen_fd;
	fd_list[1] = -1;
	client_addrlen = sizeof client_addr;
	if ( daemon_server( fd_list, (struct sockaddr *) & client_addr, & client_addrlen ) < 0 ) _exit( 1 );
    }
    argv[0][0] = 'W';

    //------------------------------------------------
    // Look up the specified server host address again
    // since it might have changed since startup.
    //------------------------------------------------
    hostent_p = gethostbyname( server_host_str );
    if ( ! hostent_p ) _exit( 1 );

    //-------------------------------------
    // Make sure this client is authorized.
    //-------------------------------------

    //---------------------
    // Open a capture file.
    //---------------------
    if ( opt_capture ) {
	size_t len;
	current_strftime_usec_local( path_str, sizeof path_str, "%Y/%m/%d/%H%M%S", "-%06lu" );
	len = strlen( path_str );
	snprintf( path_str + len, sizeof path_str - len, "-%05u", (unsigned int) ( getpid() & 0xffff ) );
	make_parent_dir( path_str, 0700 );
	capture_fd = open( path_str, O_CREAT | O_WRONLY, 0600 );
    }

    //-----------------------------------
    // Change root to am empty directory.
    //-----------------------------------

    //----------------------------------------
    // Capture where the connection came from.
    //----------------------------------------
    if ( capture_fd >= 0 ) {
	char buf[6];
	memcpy( buf + 0, & client_addr.sin_addr.s_addr, 4 );
	memcpy( buf + 4, & client_addr.sin_port, 2 );
	write_capture_data( capture_fd, 'a', buf, 6 );
    }

    //------------------------------------
    // We won't be using the duplicate fd.
    //------------------------------------
    client_fd = 0;
    close( 1 );

    //----------------------------------------
    // Connect to one of the server addresses.
    //----------------------------------------
    server_fd = -1;
    for ( server_addr_p = hostent_p->h_addr_list; * server_addr_p; ++ server_addr_p ) {
	server_addr.sin_family = hostent_p->h_addrtype;
	server_addr.sin_port = htons( server_port );
	server_addr.sin_addr = * ( (struct in_addr *) * server_addr_p );
	server_fd = socket( hostent_p->h_addrtype, SOCK_STREAM, IPPROTO_TCP );
	if ( server_fd < 0 ) {
	    continue;
	}
	if ( bind( server_fd, (struct sockaddr *) & local_addr, sizeof local_addr ) < 0 ) {
	    close( server_fd );
	    server_fd = -1;
	    continue;
	}
	if ( connect( server_fd, (struct sockaddr *) & server_addr, sizeof server_addr ) < 0 ) {
	    close( server_fd );
	    server_fd = -1;
	    continue;
	}
	break;
    }
    if ( server_fd < 0 ) _exit( 1 );

    //--------------------------------------------
    // Capture when the server connection is made.
    //--------------------------------------------
    if ( opt_capture >= 0 && server_addr.sin_family == AF_INET ) {
	char buf[6];
	memcpy( buf + 0, & server_addr.sin_addr.s_addr, 4 );
	memcpy( buf + 4, & server_addr.sin_port, 2 );
	write_capture_data( capture_fd, 'c', buf, 6 );
    }

    //-----------------------------
    // Set up virtual ring buffers.
    //-----------------------------
    if ( ! ( client_to_server_vrb = vrb_new( opt_buffersize_send, NULL ) ) ) _exit( 1 );
    if ( ! ( server_to_client_vrb = vrb_new( opt_buffersize_recv, NULL ) ) ) _exit( 1 );

    //---------------------
    // Set up polling list.
    //---------------------
    poll_list[0].fd = client_fd;
    poll_list[1].fd = server_fd;

    //-------------------------------------------------
    // Initialize EOF state:
    // 0: no EOF yet
    // 1: EOF first seen, sending side not shutdown yet
    // 2: EOF seen, sending side has now been shutdown
    //-------------------------------------------------
    server_to_client_eof = 0;
    recv_end = 0;
    client_to_server_eof = 0;
    send_end = 0;

    //------------------------------------------
    // This is the main bi-directional I/O loop.
    //------------------------------------------
    for (;;) {

	//-- Clear returned events.
	poll_list[0].revents = 0;
	poll_list[1].revents = 0;

	//-- Check for any pollable work.
	poll_list[0].events =
	    ( client_to_server_eof == 0 && vrb_space_len( client_to_server_vrb ) ? POLLIN : 0 )
	    +
	    ( vrb_data_len( server_to_client_vrb ) ? POLLOUT : 0 );
	poll_list[1].events =
	    ( server_to_client_eof == 0 && vrb_space_len( server_to_client_vrb ) ? POLLIN : 0 )
	    +
	    ( vrb_data_len( client_to_server_vrb ) ? POLLOUT : 0 );

	//-- Do poll to wait for work.
	{
	    struct pollfd *	poll_ptr	;
	    int			poll_num	;

	    poll_ptr = poll_list;
	    poll_num = 2;

	    //-- If no events in [0] then skip it.
	    if ( poll_list[0].events == 0 ) {
		-- poll_num;
		++ poll_ptr;
	    }

	    //-- If no events in [1] then skip it.
	    if ( poll_list[1].events == 0 ) {
		-- poll_num;
	    }

	    //-- If no events at all, quit.
	    if ( poll_num == 0 ) break;

	    //-- Wait for one or more events.
	    poll_num = poll( poll_ptr, poll_num, -1 );
	    if ( poll_num == 0 ) continue;
	    if ( poll_num < 0 && errno != EINTR ) break;
	}

	//-- Do indicated work.
	if ( ( poll_list[0].events & POLLIN ) && ( poll_list[0].revents & ( POLLIN | POLLERR | POLLHUP ) ) ) {
	    ssize_t	len	;
	    len = read( client_fd, vrb_space_ptr( client_to_server_vrb ), vrb_space_len( client_to_server_vrb ) );
	    if ( len <= 0 ) {
		client_to_server_eof = 1;
		write_capture_data( capture_fd, 's', NULL, 0 );
		shutdown( client_fd, SHUT_RD );
	    } else {
		if ( capture_fd >= 0 )
		    write_capture_data( capture_fd, 's', vrb_space_ptr( client_to_server_vrb ), len );
		vrb_give( client_to_server_vrb, len );
	    }
	}
	if ( ( poll_list[1].events & POLLIN ) && ( poll_list[1].revents & ( POLLIN | POLLERR | POLLHUP ) ) ) {
	    ssize_t	len	;
	    len = read( server_fd, vrb_space_ptr( server_to_client_vrb ), vrb_space_len( server_to_client_vrb ) );
	    if ( len <= 0 ) {
		server_to_client_eof = 1;
		write_capture_data( capture_fd, 'r', NULL, 0 );
		shutdown( server_fd, SHUT_RD );
	    } else {
		if ( capture_fd >= 0 )
		    write_capture_data( capture_fd, 'r', vrb_space_ptr( server_to_client_vrb ), len );
		vrb_give( server_to_client_vrb, len );
	    }
	}
	if ( ( poll_list[0].events & POLLOUT ) && ( poll_list[0].revents & ( POLLOUT | POLLERR | POLLHUP ) ) ) {
	    ssize_t	len	;
	    len = write( client_fd, vrb_data_ptr( server_to_client_vrb ), vrb_data_len( server_to_client_vrb ) );
	    if ( len < 0 ) {
		recv_end = 1;
		if ( server_to_client_eof == 0 ) {
		    server_to_client_eof = 1;
		    shutdown( server_fd, SHUT_RD );
		}
		vrb_empty( server_to_client_vrb );
	    } else {
		vrb_take( server_to_client_vrb, len );
	    }
	}
	if ( ( poll_list[1].events & POLLOUT ) && ( poll_list[1].revents & ( POLLOUT | POLLERR | POLLHUP ) ) ) {
	    ssize_t	len	;
	    len = write( server_fd, vrb_data_ptr( client_to_server_vrb ), vrb_data_len( client_to_server_vrb ) );
	    if ( len < 0 ) {
		send_end = 1;
		if ( client_to_server_eof == 0 ) {
		    client_to_server_eof = 1;
		    shutdown( client_fd, SHUT_RD );
		}
		vrb_empty( client_to_server_vrb );
	    } else {
		vrb_take( client_to_server_vrb, len );
	    }
	}

	//-- Clean up.
	if ( client_to_server_eof == 1 && ! vrb_data_len( client_to_server_vrb ) ) {
	    shutdown( server_fd, SHUT_WR );
	    client_to_server_eof = 2;
	    vrb_empty( client_to_server_vrb );
	}
	if ( server_to_client_eof == 1 && ! vrb_data_len( server_to_client_vrb ) ) {
	    shutdown( client_fd, SHUT_WR );
	    server_to_client_eof = 2;
	    vrb_empty( server_to_client_vrb );
	}
    }

    //----------------
    // Close and exit.
    //----------------
    close( server_fd );
    close( client_fd );

    //-----------------------
    // Capture when complete.
    //-----------------------
    if ( capture_fd >= 0 ) {
	write_capture_data( capture_fd, 'z', NULL, 0 );
	close( capture_fd );
    }

    _exit( 0 );
}

