#!/usr/bin/perl
#	$OpenBSD: remote.pl,v 1.9 2017/12/18 17:01:27 bluhm Exp $

# Copyright (c) 2010-2015 Alexander Bluhm <bluhm@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;

BEGIN {
	if ($> == 0 && $ENV{SUDO_UID}) {
		$> = $ENV{SUDO_UID};
	}
}

use File::Basename;
use File::Copy;
use Getopt::Std;
use Socket;
use Socket6;

use Client;
use Server;
use Remote;
use Packet;
require 'funcs.pl';

sub usage {
	die <<"EOF";
usage:
    remote.pl af bindaddr connectaddr connectport test-args.pl
	Only start remote relay.
    remote.pl af bindaddr connectaddr connectport bindport test-args.pl
	Only start remote relay with fixed port, needed for reuse.
    remote.pl af localaddr fakeaddr remotessh test-args.pl
	Run test with local client and server.  Remote relay is
	started automatically with ssh on remotessh.
    remote.pl af localaddr fakeaddr remotessh clientport serverport test-args.pl
	Run test with local client and server and fixed port, needed for reuse.
    -f	flush regress states
EOF
}

my $command = "$0 @ARGV";
my $test;
our %args;
if (@ARGV) {
	$test = pop;
	do $test
	    or die "Do test file $test failed: ", $@ || $!;
}
my %opts;
getopts("f", \%opts) or usage();
my($af, $domain, $protocol);
if (@ARGV) {
	$af = shift;
	$domain =
	    $af eq "inet" ? AF_INET :
	    $af eq "inet6" ? AF_INET6 :
	    die "address family must be 'inet' or 'inet6\n";
	$protocol = $args{protocol};
	$protocol = $protocol->({ %args, af => $af, domain => $domain, })
	    if ref $protocol eq 'CODE';
}
my $mode =
	@ARGV >= 3 && $ARGV[0] !~ /^\d+$/ && $ARGV[2] =~ /^\d+$/ ? "divert" :
	@ARGV >= 3 && $ARGV[0] !~ /^\d+$/ && $ARGV[2] !~ /^\d+$/ ? "auto"   :
	usage();
my($clientport, $serverport, $bindport);
if (@ARGV == 5 && $mode eq "auto") {
	($clientport, $serverport) = @ARGV[3,4];
} elsif (@ARGV == 4 && $mode eq "divert") {
	($bindport) = $ARGV[3];
} elsif (@ARGV != 3) {
	usage();
}

my $divert = $args{divert};
my ($local, $remote) = ("client", "server");
($local, $remote) = ($remote, $local) if $mode eq "divert";
($local, $remote) = ($remote, $local) if $divert =~ /reply|out/;
my ($srcaddr, $dstaddr)	= @ARGV[0,1];
($srcaddr, $dstaddr) = ($dstaddr, $srcaddr) if $mode eq "divert";
($srcaddr, $dstaddr) = ($dstaddr, $srcaddr) if $divert =~ /reply|out/;

my ($logfile, $ktracefile, $packetlog, $packetktrace);
if ($mode eq "divert") {
	$logfile	= dirname($0)."/remote.log";
	$ktracefile	= dirname($0)."/remote.ktrace";
	$packetlog	= dirname($0)."/packet.log";
	$packetktrace	= dirname($0)."/packet.ktrace";
}

my ($c, $l, $r, $s);
if ($local eq "server") {
	$l = $s = Server->new(
	    ktrace		=> $ENV{KTRACE},
	    %args,
	    %{$args{server}},
	    logfile		=> $logfile,
	    ktracefile		=> $ktracefile,
	    af			=> $af,
	    domain		=> $domain,
	    protocol		=> $protocol,
	    listenaddr		=>
		$mode ne "divert" || $divert =~ /packet/ ? $ARGV[0] :
		$af eq "inet" ? "127.0.0.1" : "::1",
	    listenport		=> $serverport || $bindport,
	    srcaddr		=> $srcaddr,
	    dstaddr		=> $dstaddr,
	) if $args{server};
}
if ($mode eq "auto") {
	$r = Remote->new(
	    %args,
	    opts		=> \%opts,
	    down		=> $args{packet} && "Shutdown Packet",
	    logfile		=> "$remote.log",
	    ktracefile		=> "$remote.ktrace",
	    testfile		=> $test,
	    af			=> $af,
	    remotessh		=> $ARGV[2],
	    bindaddr		=> $ARGV[1],
	    bindport		=> $remote eq "client" ?
		$clientport : $serverport,
	    connect		=> $remote eq "client",
	    connectaddr		=> $ARGV[0],
	    connectport		=> $s ? $s->{listenport} : 0,
	);
	$r->run->up;
	$r->loggrep(qr/^Diverted$/, 10)
	    or die "no Diverted in $r->{logfile}";
}
if ($local eq "client") {
	$l = $c = Client->new(
	    ktrace		=> $ENV{KTRACE},
	    %args,
	    %{$args{client}},
	    logfile		=> $logfile,
	    ktracefile		=> $ktracefile,
	    af			=> $af,
	    domain		=> $domain,
	    protocol		=> $protocol,
	    connectaddr		=> $ARGV[1],
	    connectport		=> $r ? $r->{listenport} : $ARGV[2],
	    bindany		=> $mode eq "divert",
	    bindaddr		=> $ARGV[0],
	    bindport		=> $clientport || $bindport,
	    srcaddr		=> $srcaddr,
	    dstaddr		=> $dstaddr,
	) if $args{client};
}
$l->{log}->print("local command: $command\n") if $l;

if ($mode eq "divert") {
	open(my $log, '<', $l->{logfile})
	    or die "Remote log file open failed: $!";
	$SIG{__DIE__} = sub {
		die @_ if $^S;
		copy($log, \*STDERR);
		warn @_;
		exit 255;
	};
	copy($log, \*STDERR);

	my ($p, $plog);
	$p = Packet->new(
	    ktrace		=> $ENV{KTRACE},
	    %args,
	    %{$args{packet}},
	    logfile		=> $packetlog,
	    ktracefile		=> $packetktrace,
	    af			=> $af,
	    domain		=> $domain,
	    bindport		=> 666,
	) if $args{packet};

	if ($p) {
		open($plog, '<', $p->{logfile})
		    or die "Remote packet log file open failed: $!";
		$SIG{__DIE__} = sub {
			die @_ if $^S;
			copy($log, \*STDERR);
			copy_prefix(ref $p, $plog, \*STDERR);
			warn @_;
			exit 255;
		};
		copy_prefix(ref $p, $plog, \*STDERR);
		$p->run;
		copy_prefix(ref $p, $plog, \*STDERR);
		$p->up;
		copy_prefix(ref $p, $plog, \*STDERR);
	}

	my @cmd = qw(pfctl -a regress -f -);
	my $pf;
	do { local $> = 0; open($pf, '|-', @cmd) }
	    or die "Open pipe to pf '@cmd' failed: $!";
	if ($local eq "server") {
		my $port = $protocol =~ /^(tcp|udp)$/ ?
		    "port $s->{listenport}" : "";
		my $divertport = $port || "port 1";  # XXX bad pf syntax
		my $divertcommand = $divert =~ /packet/ ?
		    "divert-packet port 666" :
		    "divert-to $s->{listenaddr} $divertport";
		print $pf "pass in log $af proto $protocol ".
		    "from $ARGV[1] to $ARGV[0] $port $divertcommand ".
		    "label regress\n";
	}
	if ($local eq "client") {
		my $port = $protocol =~ /^(tcp|udp)$/ ?
		    "port $ARGV[2]" : "";
		my $divertcommand = $divert =~ /packet/ ?
		    "divert-packet port 666" : "divert-reply";
		print $pf "pass out log $af proto $protocol ".
		    "from $c->{bindaddr} to $ARGV[1] $port $divertcommand ".
		    "label regress\n";
	}
	close($pf) or die $! ?
	    "Close pipe to pf '@cmd' failed: $!" :
	    "pf '@cmd' failed: $?";
	if ($opts{f}) {
		@cmd = qw(pfctl -k label -k regress);
		do { local $> = 0; system(@cmd) }
		    and die "Execute '@cmd' failed: $!";
	}
	print STDERR "Diverted\n";

	$l->run;
	copy($log, \*STDERR);
	$l->up;
	copy($log, \*STDERR);
	$l->down;
	copy($log, \*STDERR);

	if ($p) {
		copy_prefix(ref $p, $plog, \*STDERR);
		$p->down;
		copy_prefix(ref $p, $plog, \*STDERR);
	}

	exit;
}

$s->run if $s;
$c->run->up if $c;
$s->up if $s;

$c->down if $c;
# remote side has 20 seconds timeout, wait longer than that here
$r->down(30) if $r;
$s->down if $s;

check_logs($c || $r, $r, $s || $r, %args);

sub copy_prefix {
	my ($prefix, $src, $dst) = @_;

	local $_;
	while (defined($_ = <$src>)) {
		chomp;
		print $dst "$prefix: $_\n" if length;
	}
}
