#!/usr/bin/perl

use strict;
use warnings;
use Socket;
use UUID;
use BSD::Resource;
use Getopt::Long;
use Socket6;
use Bencode qw( bencode bdecode );
use Time::HiRes;
use Crypt::Rijndael;
use Digest::SHA qw(hmac_sha1);
use MIME::Base64;
use Data::Dumper;
use NGCP::Rtpclient::SRTP;

my ($NUM, $RUNTIME, $STREAMS, $PAYLOAD, $INTERVAL, $RTCP_INTERVAL, $STATS_INTERVAL)
	= (1000, 30, 1, 160, 20, 5, 5);
my ($NODEL, $IP, $IPV6, $KEEPGOING, $REINVITES, $PROTOS, $DEST, $SUITES, $NOENC, $RTCPMUX, $BUNDLE, $LAZY,
	$CHANGE_SSRC, $PORT_LATCHING, $RECORD, $DTMF);
GetOptions(
		'no-delete'	=> \$NODEL,
		'num-calls=i'	=> \$NUM,
		'local-ip=s'	=> \$IP,
		'local-ipv6=s'	=> \$IPV6,
		'runtime=i'	=> \$RUNTIME,
		'keep-going'	=> \$KEEPGOING,		# don't stop sending rtp if a packet doesn't go through
		'reinvites'	=> \$REINVITES,
		'max-streams=i'	=> \$STREAMS,
		'protocols=s'	=> \$PROTOS,		# "RTP/AVP,RTP/SAVP"
		'destination=s'	=> \$DEST,
		'payload-size=i'=> \$PAYLOAD,
		'rtp-interval=i'=> \$INTERVAL,		# in ms
		'rtcp-interval=i'=>\$RTCP_INTERVAL,	# in seconds
		'stats-interval=i'=>\$STATS_INTERVAL,
		'suites=s'	=> \$SUITES,
		'no-encrypt'	=> \$NOENC,
		'rtcp-mux'	=> \$RTCPMUX,
		'bundle'	=> \$BUNDLE,
		'lazy-params'	=> \$LAZY,
		'change-ssrc'   => \$CHANGE_SSRC,
		'port-latching' => \$PORT_LATCHING,
		'record'	=> \$RECORD,
		'dtmf'		=> \$DTMF,
) or die;

($IP || $IPV6) or die("at least one of --local-ip or --local-ipv6 must be given");

local $SIG{ALRM} = sub { print "alarm!\n"; };
setrlimit(RLIMIT_NOFILE, 8000, 8000);

$PROTOS and $PROTOS = [split(/\s*[,;:]+\s*/, $PROTOS)];
$$PROTOS[1] = $$PROTOS[0] if $PROTOS && @$PROTOS == 1;
$DEST and $DEST = [$DEST =~ /^(?:([a-z.-]+)(?::(\d+))?|([\d.]+)(?::(\d+))?|([\da-f:]+)|\[([\da-f:]+)\]:(\d+))$/si];
my $dest_host = $$DEST[0] || $$DEST[2] || $$DEST[4] || $$DEST[5] || 'localhost';
my $dest_port = $$DEST[1] || $$DEST[3] || $$DEST[6] || 2223;
$SUITES and $SUITES = [split(/\s*[,;:]+\s*/, $SUITES)];

my @chrs = ('a' .. 'z', 'A' .. 'Z', '0' .. '9');
sub rand_str {
	my ($len) = @_;
	return join('', (map {$chrs[rand(@chrs)]} (1 .. $len)));
}

my $fd;
sub msg {
	my ($d) = @_;
	my $l = bencode($d);
	my $cookie = $$ . '_' . rand_str(10);
	my $r;
	while (1) {
		send($fd, "$cookie $l", 0) or die $!;
		my $err = '';
		alarm(1);
		recv($fd, $r, 0xffff, 0) or $err = "$!";
		alarm(0);
		$err =~ /interrupt/i and next;
		$err and die $err;
		last;
	}
	$r =~ s/^\Q$cookie\E +//s or die $r;
	$r =~ s/[\r\n]+$//s;
	return $r ? bdecode($r, 1) : undef;
}

my @dests = getaddrinfo($dest_host, $dest_port, AF_UNSPEC, SOCK_DGRAM);
while (@dests >= 5) {
	my ($fam, $type, $prot, $addr, $canon, @dests) = @dests;
	if (!socket($fd, $fam, $type, $prot)) {
		undef($fd);
		next;
	}
	if (!connect($fd, $addr)) {
		undef($fd);
		next;
	}
	last;
}
$fd or die($!);

msg({command => 'ping'})->{result} eq 'pong' or die;

my (@calls, %calls);
my %NOENC;

sub send_receive {
	my ($send_fd, $receive_fd, $payload, $destination) = @_;

	send($send_fd, $payload, 0, $destination) or die $!;
	my $x;
	my $err = '';
	alarm(1);
	recv($receive_fd, $x, 0xffff, 0) or $err = "$!";
	alarm(0);
	die $err if $err && $err !~ /interrupt/i;
	return $x;
}

sub rtcp_encrypt {
	my ($r, $ctx, $dir) = @_;

	my $dctx = $$ctx{$dir};

	if (!$$dctx{rtcp_session_key}) {
		($$dctx{rtcp_session_key}, $$dctx{rtcp_session_auth_key}, $$dctx{rtcp_session_salt})
			= NGCP::Rtpclient::SRTP::gen_rtcp_session_keys($$dctx{rtp_master_key},
				$$dctx{rtp_master_salt});
	}

	($NOENC && $NOENC{rtcp_packet}) and return $NOENC{rtcp_packet};

	my ($pkt, $idx) = NGCP::Rtpclient::SRTP::encrypt_rtcp(@$dctx{qw(crypto_suite rtcp_session_key
		rtcp_session_salt rtcp_session_auth_key rtcp_index rtp_mki rtp_mki_len unenc_srtcp)}, $r);
	$$dctx{rtcp_index} = $idx;

	$NOENC{rtcp_packet} = $pkt;

	return $pkt;
}

sub rtp_encrypt {
	my ($r, $ctx, $dir) = @_;

	my $dctx = $$ctx{$dir};

	if (!$$dctx{rtp_session_key}) {
		($$dctx{rtp_session_key}, $$dctx{rtp_session_auth_key}, $$dctx{rtp_session_salt})
			= NGCP::Rtpclient::SRTP::gen_rtp_session_keys($$dctx{rtp_master_key},
				$$dctx{rtp_master_salt});
	}

	($NOENC && $NOENC{rtp_packet}) and return $NOENC{rtp_packet};

	my ($pkt, $roc) = NGCP::Rtpclient::SRTP::encrypt_rtp(@$dctx{qw(crypto_suite rtp_session_key
		rtp_session_salt rtp_session_auth_key rtp_roc rtp_mki rtp_mki_len unenc_srtp unauth_srtp)}, $r);
	$roc == ($$dctx{rtp_roc} // 0) or print("ROC is now $roc\n");
	$$dctx{rtp_roc} = $roc;

	$NOENC{rtp_packet} = $pkt;

	return $pkt;
}

$SUITES and @NGCP::Rtpclient::SRTP::crypto_suites = grep {my $x = $$_{str}; grep {$x eq $_} @$SUITES}
	@NGCP::Rtpclient::SRTP::crypto_suites;

sub savp_sdp {
	my ($ctx, $ctx_o) = @_;

	if (!$$ctx{out}{crypto_suite}) {
		if ($$ctx{in}{crypto_suite}) {
			$$ctx{out}{crypto_suite} = $$ctx{in}{crypto_suite};
			$$ctx{out}{crypto_tag} = $$ctx{in}{crypto_tag};
			$$ctx{out}{unenc_srtp} = $$ctx{in}{unenc_srtp};
			$$ctx{out}{unenc_srtcp} = $$ctx{in}{unenc_srtcp};
			$$ctx{out}{unauth_srtp} = $$ctx{in}{unauth_srtp};
		}
		else {
			$$ctx{out}{crypto_suite} =
				$NGCP::Rtpclient::SRTP::crypto_suites[rand(@NGCP::Rtpclient::SRTP::crypto_suites)];
			print("using crypto suite $$ctx{out}{crypto_suite}{str}\n");
			$$ctx{out}{crypto_tag} = int(rand(100));
			$$ctx{out}{unenc_srtp} = rand() < .5 ? 0 : 1;
			$$ctx{out}{unenc_srtcp} = rand() < .5 ? 0 : 1;
			$$ctx{out}{unauth_srtp} = rand() < .5 ? 0 : 1;
		}

		$$ctx{out}{rtp_mki_len} = 0;
		if (rand() > .5) {
			$$ctx{out}{rtp_mki_len} = int(rand(120)) + 1;
			$$ctx{out}{rtp_mki} = int(rand(2**30)) | 1;
			if ($$ctx{out}{rtp_mki_len} < 32) {
				$$ctx{out}{rtp_mki} &= (0xffffffff >> (32 - ($$ctx{out}{rtp_mki_len})));
			}
		}
	}

	if (!$$ctx{out}{rtp_master_key} || rand() < .2) {
		$$ctx{out}{rtp_master_key} and print("new key\n");
		$$ctx{out}{rtp_master_key} = rand_str($$ctx{out}{crypto_suite}{key_length});
		$$ctx{out}{rtp_master_salt} = rand_str($$ctx{out}{crypto_suite}{salt_length});
		undef($$ctx{out}{rtp_session_key});
		undef($$ctx{out}{rtcp_session_key});
		if ($NOENC && $NOENC{rtp_master_key}) {
			$$ctx{out}{rtp_master_key} = $NOENC{rtp_master_key};
			$$ctx{out}{rtp_master_salt} = $NOENC{rtp_master_salt};
		}
		$NOENC{rtp_master_key} = $$ctx{out}{rtp_master_key};
		$NOENC{rtp_master_salt} = $$ctx{out}{rtp_master_salt};
	}

	my $ret = "a=crypto:$$ctx{out}{crypto_tag} $$ctx{out}{crypto_suite}{str} inline:" . encode_base64($$ctx{out}{rtp_master_key} . $$ctx{out}{rtp_master_salt}, '');
	$$ctx{out}{rtp_mki_len} and $ret .= "|$$ctx{out}{rtp_mki}:$$ctx{out}{rtp_mki_len}";

	$$ctx{out}{unenc_srtp} and $ret .= " UNENCRYPTED_SRTP";
	$$ctx{out}{unenc_srtcp} and $ret .= " UNENCRYPTED_SRTCP";
	$$ctx{out}{unauth_srtp} and $ret .= " UNAUTHENTICATED_SRTP";

	$ret .= "\n";
	return $ret;
}

sub rtcp_sr {
	my ($ssrc) = @_;
	my @now = Time::HiRes::gettimeofday();
	my $secs = $now[0] + 2208988800;
	my $frac = $now[1] / 1000000 * 2**32;
	my $sr = pack('CCnN NNN NN', (2 << 6) | 1, 200, 12, $ssrc, $secs, $frac,
		12345, rand(12345), rand(4321));
	$sr .= pack('N CCCC NNNN', rand(2**32), rand(256), rand(256), rand(256), rand(256),
		rand(2**32), rand(2**32), rand(2**32), rand(2**32));
	# sdes
	$sr .= pack('CCn N CC a* CC a* CC a* C C N CC a* CC a* C CCC N CC a* C',
		(2 << 6) | 3, 202, 16,
		rand(2 ** 32), # csrc
		1, 7, 'blah123', # cname
		2, 6, 'foobar', # name
		3, 7, 'foo@bar', # email,
		0, # eol
		0, # padding
		rand(2 ** 32), # csrc
		4, 5, '54321', # phone
		5, 3, 'foo', # loc
		0, # eol
		0,0,0, # padding
		rand(2 ** 32), # csrc
		6, 5, 'fubar', # tool
		0, # eol
	);
	return $sr;
}

sub rtcp_rtpfb {
	return pack('CCn NN', (2 << 6) | 1, 205, 2, rand() * 2**32, rand() * 2**32);
}

sub rtcp_avp {
	my ($recv, $ctx, $ctx_o) = @_;
	my $ssrc = $$ctx{ssrc} // ($$ctx{ssrc} = rand(2**32));
	my $sr = rtcp_sr($ssrc);
	my $exp = $sr;
	$$recv{srtp} and $exp = rtcp_encrypt($exp, $ctx_o, 'in');
	return ($sr, $exp);
}

sub rtcp_savp {
	my ($recv, $ctx, $ctx_o) = @_;
	my $ssrc = $$ctx{ssrc} // ($$ctx{ssrc} = rand(2**32));
	my $sr = rtcp_sr($ssrc);
	my $enc = rtcp_encrypt($sr, $ctx, 'out');
	my $exp = $sr;
	$$recv{srtp} and $exp = rtcp_encrypt($exp, $ctx_o, 'in');
	return ($enc, $exp);
}

sub rtcp_avpf {
	my ($recv, $ctx, $ctx_o) = @_;
	my $ssrc = $$ctx{ssrc} // ($$ctx{ssrc} = rand(2**32));
	my $sr = rtcp_sr($ssrc);
	my $fb = rtcp_rtpfb();
	my $exp = $sr;
	$$recv{avpf} and $exp .= $fb;
	$$recv{srtp} and $exp = rtcp_encrypt($exp, $ctx_o, 'in');
	return ($sr . $fb, $exp);
}

sub rtcp_savpf {
	my ($recv, $ctx, $ctx_o) = @_;
	my $ssrc = $$ctx{ssrc} // ($$ctx{ssrc} = rand(2**32));
	my $sr = rtcp_sr($ssrc);
	my $fb = rtcp_rtpfb();
	my $enc = rtcp_encrypt($sr . $fb, $ctx, 'out');
	my $exp = $sr;
	$$recv{avpf} and $exp .= $fb;
	$$recv{srtp} and $exp = rtcp_encrypt($exp, $ctx_o, 'in');
	return ($enc, $exp);
}

sub rtp {
	my ($ctx) = @_;
	my $ssrc = $$ctx{ssrc} // ($$ctx{ssrc} = rand(2**32));
	my $seq = $$ctx{rtp_seqnum};
	defined($seq) or $seq = int(rand(0xfffe)) + 1;
	my ($hdr, $payload);
	# don't do DTMF as first packet XXX should work
	if ($$ctx{out_count} && ($$ctx{is_dtmf} || ($DTMF && rand() < 0.1))) {
		# DTMF in progress or start it
		if (!$$ctx{is_dtmf}) {
			# start DTMF
			$$ctx{is_dtmf} = 1;
		}
		# else: DTMF in progress
		$hdr = pack("CCnNN", 0x80, 96, $seq, rand(2**32), $ssrc);
		if (rand() < 0.2) {
			# end DTMF
			$$ctx{is_dtmf} = 0;
			$payload = pack("CCN", rand(10), 0x80 | rand(50)+10, rand(2**16));
			# XXX triple-send end packet
		}
		else {
			$payload = pack("CCN", rand(10), rand(50)+10, rand(2**16));
		}
	}
	else {
		# random PCM
		$hdr = pack("CCnNN", 0x80, 0, $seq, rand(2**32), $ssrc);
		$payload = rand_str($PAYLOAD);
	}
	my $pack = $hdr . $payload;
	$$ctx{rtp_seqnum} = (++$seq & 0xffff);
	$$ctx{out_count}++;
	return $pack;
}

sub rtp_avp {
	my ($recv, $ctx, $ctx_o) = @_;
	my $pack = rtp($ctx);
	my $exp = $pack;
	$$recv{srtp} and $exp = rtp_encrypt($exp, $ctx_o, 'in');
	return ($pack, $exp);
}

sub rtp_savp {
	my ($recv, $ctx, $ctx_o) = @_;
	my $pack = rtp($ctx);
	my $enc = rtp_encrypt($pack, $ctx, 'out');
	my $exp = $pack;
	$$recv{srtp} and $exp = rtp_encrypt($pack, $ctx_o, 'in');
	return ($enc, $exp);
}

sub savp_crypto {
	my ($sdp, $ctx, $ctx_o) = @_;

	my @aa = $sdp =~ /[\r\n](?:a=crypto:(\d+) (\w+) inline:([\w\/+=]{40,})(?:\|(?:2\^(\d+)|(\d+)))?(?:\|(\d+):(\d+))?(?: (.*?))?|(m)=.*?)[\r\n]/sig;
	@aa or die;
	my $i = -1;
	my @done;
	while (@aa >= 9) {
		if (defined($aa[8]) && $aa[8] eq 'm') {
			$i++;
			next;
		}
		$i >= 0 or die;
		$done[$i] and next;

		$$ctx[$i]{in}{crypto_suite} = $NGCP::Rtpclient::SRTP::crypto_suites{$aa[1]} or next;
		$$ctx[$i]{in}{crypto_tag} = $aa[0];
		($$ctx[$i]{in}{rtp_master_key}, $$ctx[$i]{in}{rtp_master_salt})
			= NGCP::Rtpclient::SRTP::decode_inline_base64($aa[2], $$ctx[$i]{in}{crypto_suite});
		$$ctx[$i]{in}{rtp_mki} = $aa[5];
		$$ctx[$i]{in}{rtp_mki_len} = $aa[6];
		undef($$ctx[$i]{in}{rtp_session_key});
		undef($$ctx[$i]{in}{rtcp_session_key});
		($aa[7] || '') =~ /UNENCRYPTED_SRTP/ and $$ctx[$i]{in}{unenc_srtp} = 1;
		($aa[7] || '') =~ /UNENCRYPTED_SRTCP/ and $$ctx[$i]{in}{unenc_srtcp} = 1;
		($aa[7] || '') =~ /UNAUTHENTICATED_SRTP/ and $$ctx[$i]{in}{unauth_srtp} = 1;

		$done[$i] = 1;
	}
	continue {
		@aa = @aa[9 .. $#aa];
	}
}

sub hexdump {
	my $o = '';
	for my $a (@_) {
		$o .= "<< " . unpack("H*", $a) . " >> ";
	}
	return $o;
}

my $RTP_COUNT = 0;

sub do_rtp {
	my ($rtcp) = @_;
	for my $c (@calls) {
		$c or next;
		for my $i ([0,1],[1,0]) {
			my ($a, $b) = @$i;
			my $A = $$c{sides}[$a];
			my $B = $$c{sides}[$b];

			my $rtp_fds = $$A{rtp_fds};
			my $rtcp_fds = $$A{rtcp_fds};
			my $rtp_fds_o = $$B{rtp_fds};
			my $rtcp_fds_o = $$B{rtcp_fds};

			my $pr = $$A{proto};;
			my $trans = $$A{transport};
			my $trans_o = $$B{transport};
			my $tcx = $$A{trans_contexts};
			my $tcx_o = $$B{trans_contexts};
			my $outputs = $$A{outputs};

			for my $j (0 .. ($$A{streams_active} - 1)) {
				my ($bj_a, $bj_b) = ($j, $j);
				$$A{bundle}
					and $bj_a = 0;
				$$B{bundle}
					and $bj_b = 0;

				my $addr = inet_pton($$pr{family}, $$outputs[$j][1]);
				my ($payload, $expect) = $$trans{rtp_func}($trans_o, $$tcx[$j], $$tcx_o[$j]);
				my $dst = $$pr{sockaddr}($$outputs[$j][0], $addr);
				my $repl = send_receive($$rtp_fds[$bj_a], $$rtp_fds_o[$bj_b], $payload, $dst);
				$RTP_COUNT++;
				if ($repl eq '') {
					warn("no rtp reply received, port $$outputs[$j][0]");
					$KEEPGOING or undef($c);
				}
				$NOENC and $repl = $expect;
				next if !$repl && $KEEPGOING;
				$repl eq $expect or die hexdump($repl, $expect) . " $$trans{name} > $$trans_o{name}, $$c{callid}, RTP port $$outputs[$j][0]";

				$rtcp or next;
				($payload, $expect) = $$trans{rtcp_func}($trans_o, $$tcx[$j], $$tcx_o[$j]);
				my $dstport = $$outputs[$j][0] + 1;
				my $sendfd = $$rtcp_fds[$bj_a];
				my $expfd = $$rtcp_fds_o[$bj_b];
				if ($$A{rtcpmux}) {
					$dstport--;
					$sendfd = $$rtp_fds[$bj_a];
				}
				if ($$B{rtcpmux}) {
					$expfd = $$rtp_fds_o[$bj_b];
				}
				$dst = $$pr{sockaddr}($dstport, $addr);
				$repl = send_receive($sendfd, $expfd, $payload, $dst);
				$NOENC and $repl = $expect;
				next if !$repl && $KEEPGOING;
				$repl eq $expect or die hexdump($repl, $expect) . " $$trans{name} > $$trans_o{name}, $$c{callid}, RTCP";
			}
		}
	}
}

my %proto_defs = (
	ipv4 => {
		code		=> 'I',
		family		=> AF_INET,
		reply		=> '4',
		address		=> $IP,
		sockaddr	=> \&sockaddr_in,
		family_str	=> 'IP4',
		direction	=> 'internal',
	},
	ipv6 => {
		code		=> 'E',
		family		=> AF_INET6,
		reply		=> '6',
		address		=> $IPV6,
		sockaddr	=> \&sockaddr_in6,
		family_str	=> 'IP6',
		direction	=> 'external',
	},
);
my @protos_avail;
$IP and push(@protos_avail, $proto_defs{ipv4});
$IPV6 and push(@protos_avail, $proto_defs{ipv6});
my @sides = qw(A B);

my @transports = (
	{
		name => 'RTP/AVP',
		rtp_func => \&rtp_avp,
		rtcp_func => \&rtcp_avp,
		srtp => 0,
		avpf => 0,
	},
	{
		name => 'RTP/AVPF',
		rtp_func => \&rtp_avp,
		rtcp_func => \&rtcp_avpf,
		srtp => 0,
		avpf => 1,
	},
	{
		name => 'RTP/SAVP',
		sdp_media_params => \&savp_sdp,
		sdp_parse_func => \&savp_crypto,
		rtp_func => \&rtp_savp,
		rtcp_func => \&rtcp_savp,
		srtp => 1,
		avpf => 0,
	},
	{
		name => 'RTP/SAVPF',
		sdp_media_params => \&savp_sdp,
		sdp_parse_func => \&savp_crypto,
		rtp_func => \&rtp_savp,
		rtcp_func => \&rtcp_savpf,
		srtp => 1,
		avpf => 1,
	},
	{
		name => 'UDP/TLS/RTP/SAVP',
		sdp_media_params => \&savp_sdp,
		sdp_parse_func => \&savp_crypto,
		rtp_func => \&rtp_savp,
		rtcp_func => \&rtcp_savp,
		srtp => 1,
		avpf => 0,
	},
	{
		name => 'UDP/TLS/RTP/SAVPF',
		sdp_media_params => \&savp_sdp,
		sdp_parse_func => \&savp_crypto,
		rtp_func => \&rtp_savp,
		rtcp_func => \&rtcp_savpf,
		srtp => 1,
		avpf => 1,
	},
);
my %transports = map {$$_{name} => $_} @transports;

sub callid {
	my $i = rand_str(50);
	return $i;
}

my $NUM_STREAMS = 0;

sub port_setup {
	my ($r, $j) = @_;

	my $pr = $$r{proto};
	my $rtp_fds = $$r{rtp_fds};
	my $rtcp_fds = $$r{rtcp_fds};
	my $ports = $$r{ports};
	my $ips = $$r{ips};
	my $tcx = $$r{trans_contexts};
	$$tcx[$j] or $$tcx[$j] = {};

	while (1) {
		socket(my $rtp, $$pr{family}, SOCK_DGRAM, 0) or die $!;
		socket(my $rtcp, $$pr{family}, SOCK_DGRAM, 0) or die $!;
		my $port = (rand(0x7000) << 1) + 1024;
		bind($rtp, $$pr{sockaddr}($port,
			inet_pton($$pr{family}, $$pr{address}))) or next;
		bind($rtcp, $$pr{sockaddr}($port + 1,
			inet_pton($$pr{family}, $$pr{address}))) or next;

		$$rtp_fds[$j] = $rtp;
		$$rtcp_fds[$j] = $rtcp;

		my $addr = getsockname($rtp);
		my $ip;
		($$ports[$j], $ip) = $$pr{sockaddr}($addr);
		$$ips[$j] = inet_ntop($$pr{family}, $ip);

		last;
	}
}

sub side_setup {
	my ($i) = @_;
	my $r = {};

	my $pr = $$r{proto} = $protos_avail[rand(@protos_avail)];
	$$r{transport} = ($PROTOS && $$PROTOS[$i] && $transports{$$PROTOS[$i]})
			? $transports{$$PROTOS[$i]}
			: $transports[rand(@transports)];
	$$r{trans_contexts} = [];
	$$r{outputs} = [];

	$$r{num_streams} = int(rand($STREAMS));
	$$r{streams_seen} = 0;
	$$r{streams_active} = 0;
	$$r{rtp_fds} = [];
	$$r{rtcp_fds} = [];
	$$r{ports} = [];
	$$r{ips} = [];

	for my $j (0 .. $$r{num_streams}) {
		port_setup($r, $j);
	}

	$$r{tag} = rand_str(15);
	$RTCPMUX and $$r{want_rtcpmux} = rand() >= .3;
	$BUNDLE and $$r{want_bundle} = rand() >= .3;
	$$r{want_bundle} and $$r{want_rtcpmux} = 1;

	return $r;
}

sub call_setup {
	my ($c) = @_;

	$$c{setup} = 1;
	$$c{callid} = callid();

	$$c{sides}[0] = side_setup(0);
	$$c{sides}[1] = side_setup(1);
}

sub offer_answer {
	my ($c, $a, $b, $op) = @_;

	$$c{setup} or call_setup($c);

	my $callid = $$c{callid} || ($$c{callid} = callid());

	my $A = $$c{sides}[$a];
	my $B = $$c{sides}[$b];

	my $pr = $$A{proto};
	my $pr_o = $$B{proto};
	my $ips_t = $$A{ips};
	my $ports_t = $$A{ports};
	my $tr = $$A{transport};
	my $tr_o = $$B{transport};
	my $tcx = $$A{trans_contexts};
	my $tcx_o = $$B{trans_contexts};

	my $sdp = <<"SDP";
v=0
o=blah 123 123 IN $$pr{family_str} $$ips_t[0]
s=session
c=IN $$pr{family_str} $$ips_t[0]
t=0 0
SDP
	my $ul = $$A{num_streams};
	$ul = $$A{streams_seen} if $op eq 'answer' && $$A{streams_seen} < $$A{num_streams};

	if ($$A{want_bundle} && $op eq 'offer') {
		$$A{bundle} = 1;
		$sdp .= "a=group:BUNDLE " . join(' ', (0 .. $ul)) . "\n";
	}

	for my $i (0 .. $ul) {
		my $bi = $i;
		$$A{bundle}
			and $bi = 0;

		my $p = $$ports_t[$bi];
		my $cp = $p + 1;
		$cp = $p if $$A{bundle} && $$A{want_rtcpmux} && $op eq 'offer';

		$sdp .= "m=audio $p $$tr{name} 0 8 111";
		$DTMF and $sdp .= ' 96';
		$sdp .= "\n";
		$sdp .= <<"SDP";
a=rtpmap:8 PCMA/8000
a=rtpmap:111 opus/48000/2
SDP
		$DTMF and $sdp .= "a=rtpmap:96 telephone-event/8000\n";

		if ($$A{want_rtcpmux} && $op eq 'offer') {
			$sdp .= "a=rtcp-mux\n";
			$sdp .= "a=rtcp:$cp\n";
			$$A{rtcpmux} = 1;
		}
		else {
			rand() >= .5 and $sdp .= "a=rtcp:$cp\n";
		}
		$$tr{sdp_media_params} and $sdp .= $$tr{sdp_media_params}($$tcx[$i], $$tcx_o[$i]);

		$$A{bundle} and
			$sdp .= "a=mid:$i\n";
	}

	for my $x (($ul + 1) .. $$A{streams_seen}) {
		$sdp .= "m=audio 0 $$tr{name} 0\n";
	}

	$op eq 'offer' and print("transport is $$tr{name} -> $$tr_o{name}\n");

	#print(Dumper($op, $A, $B, $sdp) . "\n\n\n\n");
	#print("sdp $op in:\n$sdp\n\n");

	my @flags = ('trust address');
	my $dict = {sdp => $sdp, command => $op, 'call-id' => $$c{callid},
		flags => \@flags,
		replace => [ 'origin', 'session connection' ],
		#direction => [ $$pr{direction}, $$pr_o{direction} ],
		'received from' => [ qw(IP4 127.0.0.1) ],
		'rtcp-mux' => ['demux'],
		label => rand(),
	};
	$PORT_LATCHING and push(@flags, 'port latching');
	$RECORD and push(@flags, 'record call');
	#$viabranch and $dict->{'via-branch'} = $viabranch;
	if ($op eq 'offer') {
		$dict->{'from-tag'} = $$A{tag};
		rand() > .5 and $$dict{'to-tag'} = $$B{tag};
	}
	elsif ($op eq 'answer') {
		$dict->{'from-tag'} = $$B{tag};
		$dict->{'to-tag'} = $$A{tag};
	}
	if (!$LAZY
		|| ($op eq 'offer' && !$$c{established})
		|| (rand() > .5))
	{
		$$dict{'address family'} = $$pr_o{family_str};
		$$dict{'transport protocol'} = $$tr_o{name};

		if ($$tr_o{srtp} && $op eq 'offer') {
			my (@opts, @opt);
			rand() < .5 and push(@opts, (qw(unencrypted_srtp encrypted_srtp))[rand(2)]);
			rand() < .5 and push(@opts, (qw(unencrypted_srtcp encrypted_srtcp))[rand(2)]);
			rand() < .5 and push(@opts, (qw(unauthenticated_srtp authenticated_srtp))[rand(2)]);
			$$dict{SDES} = \@opts;
		}
	}

	#print(Dumper($dict) . "\n\n");
	my $o = msg($dict);

	$$o{result} eq 'ok' or die;
	#print("sdp $op out:\n$$o{sdp}\n\n\n\n");
	my ($rp_af, $rp_add) = $$o{sdp} =~ /c=IN IP([46]) (\S+)/s or die;
	$$B{rtcpmux} and ($$o{sdp} =~ /a=rtcp-mux/s or die);
	my @rp_ports = $$o{sdp} =~ /m=audio (\d+) \Q$$tr_o{name}\E /gs or die;
	$$B{streams_seen} = $#rp_ports;
	$rp_af ne $$pr_o{reply} and die "incorrect address family reply code";
	$NUM_STREAMS -= $$B{streams_active};
	$$B{streams_active} = 0;
	my $old_outputs = $$B{outputs};
	my $rpl_t = $$B{outputs} = [];
	for my $i (0 .. $#rp_ports) {
		my $rpl = $rp_ports[$i];

		if ($rpl == 0) {
			$op eq 'offer' and $$B{streams_seen}--;
			if ($$A{rtp_fds}[$i]) {
				undef($$A{rtp_fds}[$i]);
			}
			next;
		}

		$$B{ports}[$i] or next;

		$$B{streams_active}++;
		$NUM_STREAMS++;
		push(@$rpl_t, [$rpl,$rp_add]);
		my $oa = shift(@$old_outputs);
		if (defined($oa) && $$oa[0] != $rpl) {
			print("port change: $$oa[0] -> $rpl\n");
			#print(Dumper($i, $c) . "\n");
			undef($$tcx_o[$i]{out}{rtcp_index});
			undef($$tcx_o[$i]{out}{rtp_roc});
		}
	}
	$$tr_o{sdp_parse_func} and $$tr_o{sdp_parse_func}($$o{sdp}, $tcx_o, $tcx);
	#print(Dumper($op, $A, $B) . "\n\n\n\n");

	$op eq 'answer' and $$c{established} = 1;
}

sub offer {
	my ($c, $a, $b) = @_;
	return offer_answer($c, $a, $b, 'offer');
}
sub answer {
	my ($c, $a, $b) = @_;
	return offer_answer($c, $a, $b, 'answer');
}

for my $iter (1 .. $NUM) {
	if ($iter % 10 == 0) {
		print("$iter calls established\n");
		do_rtp();
	}

	my $c = {};
	offer($c, 0, 1);
	answer($c, 1, 0);
	push(@calls, $c);
	$calls{$$c{callid}} = $c;
}

print("all calls established\n");

#print(Dumper(\@calls) . "\n");

my $end = time() + $RUNTIME;
my $rtptime = Time::HiRes::gettimeofday();
my $rtcptime = $rtptime;
my $countstart = $rtptime;
my $countstop = $countstart + $STATS_INTERVAL;
my $last_reinv = $rtptime;
while (time() < $end) {
	my $now = Time::HiRes::gettimeofday();
	$now <= $rtptime and Time::HiRes::sleep($rtptime - $now);
	$rtptime += $INTERVAL / 1000.0;

	my $rtcp = 0;
	if ($now >= $rtcptime) {
		$rtcp = 1;
		$rtcptime += $RTCP_INTERVAL;
	}

	if ($now >= $countstop) {
		my $span = $now - $countstart;
		printf("[%05d] %d RTP packets sent in %.1f seconds = %.1f packets per stream per second\n",
			$$,
			$RTP_COUNT, $span,
			$RTP_COUNT / $span / $NUM_STREAMS);
		$RTP_COUNT = 0;
		$countstart = $now;
		$countstop = $countstart + $STATS_INTERVAL;
	}

	do_rtp($rtcp);

	@calls = sort { rand() < .5 } grep { defined } @calls;

	if ($REINVITES && $now >= $last_reinv + 15) {
		$last_reinv = $now;
		my $c = $calls[rand(@calls)];
		print("simulating re-invite on $$c{callid}\n");
		for my $i (0,1) {
			my $s = $$c{sides}[$i];
			for my $j (0 .. $$s{num_streams}) {
				if (rand() < .5) {
					print("\tside $sides[$i] stream #$j: new port\n");
					port_setup($s, $j);
					#print("\n" . Dumper($i, $c) . "\n");
					undef($$s{trans_contexts}[$j]{in}{rtcp_index});
					undef($$s{trans_contexts}[$j]{in}{rtp_roc});
				}
				else {
					print("\tside $sides[$i] stream #$j: same port\n");
				}
			}
		}
		offer($c, 0, 1);
		answer($c, 1, 0);
	}

	if ($CHANGE_SSRC && rand() < .001) {
		my $c = $calls[rand(@calls)];
		my $s = $$c{sides}[rand(2)];
		my $st = rand($$s{num_streams});
		my $d = (qw(in out))[rand(2)];
		my $stc = $$s{trans_contexts}[$st];
		my $ct = $$stc{$d};
		if (defined($$ct{rtp_roc}) && $$stc{ssrc}) {
			my $nssrc = rand(2 ** 32);
			print("change SSRC from $$stc{ssrc} to $nssrc\n");
			$$stc{ssrc} = $nssrc;
			$$ct{roc} = 0;
		}
	}
}

if (!$NODEL) {
	print("deleting\n");
	for my $c (@calls) {
		$c or next;
		my $callid = $$c{callid};
		my $fromtag = $$c{sides}[0]{tag};
		my $totag = $$c{sides}[1]{tag};
		my $dict = { command => 'delete', 'call-id' => $callid, 'from-tag' => $fromtag,
			'to-tag' => $totag,
		};
		msg($dict);
	}
}
print("done\n");
