# $OpenBSD: funcs.pl,v 1.9 2017/11/08 22:14:02 bluhm Exp $ # Copyright (c) 2010-2017 Alexander Bluhm # # 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; no warnings 'experimental::smartmatch'; use feature 'switch'; use Errno; use Digest::MD5; use IO::Socket qw(sockatmark); use Socket; use Time::HiRes qw(time alarm sleep); use BSD::Socket::Splice qw(setsplice getsplice geterror); ######################################################################## # Client funcs ######################################################################## sub write_stream { my $self = shift; my $len = shift // $self->{len} // 251; my $sleep = $self->{sleep}; my $ctx = Digest::MD5->new(); my $char = '0'; for (my $i = 1; $i < $len; $i++) { $ctx->add($char); print $char or die ref($self), " print failed: $!"; given ($char) { when(/9/) { $char = 'A' } when(/Z/) { $char = 'a' } when(/z/) { $char = "\n" } when(/\n/) { print STDERR "."; $char = '0' } default { $char++ } } if ($self->{sleep}) { IO::Handle::flush(\*STDOUT); sleep $self->{sleep}; } } if ($len) { $ctx->add("\n"); print "\n" or die ref($self), " print failed: $!"; print STDERR ".\n"; } IO::Handle::flush(\*STDOUT); print STDERR "LEN: $len\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub write_oob { my $self = shift; my $len = shift // $self->{len} // 251; my $ctx = Digest::MD5->new(); my $msg = ""; my $char = '0'; for (my $i = 1; $i < $len; $i++) { $msg .= $char; given ($char) { when(/9/) { $ctx->add("[$char]"); defined(send(STDOUT, $msg, MSG_OOB)) or die ref($self), " send OOB failed: $!"; # If tcp urgent data is sent too fast, # it may get overwritten and lost. sleep .1; $msg = ""; $char = 'A'; } when(/Z/) { $ctx->add($char); $char = 'a' } when(/z/) { $ctx->add($char); $char = "\n" } when(/\n/) { $ctx->add($char); defined(send(STDOUT, $msg, 0)) or die ref($self), " send failed: $!"; print STDERR "."; $msg = ""; $char = '0'; } default { $ctx->add($char); $char++ } } } if ($len) { $msg .= "\n"; $ctx->add("\n"); send(STDOUT, $msg, 0) or die ref($self), " send failed: $!"; print STDERR ".\n"; } IO::Handle::flush(\*STDOUT); print STDERR "LEN: $len\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub write_datagram { my $self = shift; my @lengths = @{$self->{lengths} || [ shift // $self->{len} // 251 ]}; my $sleep = $self->{sleep}; my $len = 0; my $ctx = Digest::MD5->new(); my $char = '0'; my @md5s; for (my $num = 0; $num < @lengths; $num++) { my $l = $lengths[$num]; my $string = ""; for (my $i = 1; $i < $l; $i++) { $ctx->add($char); $string .= $char; given ($char) { when(/9/) { $char = 'A' } when(/Z/) { $char = 'a' } when(/z/) { $char = "\n" } when(/\n/) { $char = '0' } default { $char++ } } } if ($l) { $ctx->add("\n"); $string .= "\n"; } defined(my $write = syswrite(STDOUT, $string)) or die ref($self), " syswrite number $num failed: $!"; $write == $l or die ref($self), " syswrite length $l did write $write"; $len += $write; print STDERR "."; sleep $self->{sleep} if $self->{sleep}; } print STDERR "\n"; print STDERR "LEN: $len\n"; print STDERR "LENGTHS: @lengths\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub solingerout { my $self = shift; setsockopt(STDOUT, SOL_SOCKET, SO_LINGER, pack('ii', 1, 0)) or die ref($self), " set linger out failed: $!"; } ######################################################################## # Relay funcs ######################################################################## sub relay_copy_stream { my $self = shift; my $max = $self->{max}; my $idle = $self->{idle}; my $size = $self->{size} || 8093; my $len = 0; while (1) { my $rin = my $win = my $ein = ''; vec($rin, fileno(STDIN), 1) = 1; vec($ein, fileno(STDIN), 1) = 1 unless $self->{oobinline}; defined(my $n = select($rin, undef, $ein, $idle)) or die ref($self), " select failed: $!"; if ($idle && $n == 0) { print STDERR "\n"; print STDERR "Timeout\n"; last; } my $buf; my $atmark = sockatmark(\*STDIN) or die ref($self), " sockatmark failed: $!"; if ($atmark == 1) { if ($self->{oobinline}) { defined(recv(STDIN, $buf, 1, 0)) or die ref($self), " recv OOB failed: $!"; $len += length($buf); defined(send(STDOUT, $buf, MSG_OOB)) or die ref($self), " send OOB failed: $!"; } else { defined(recv(STDIN, $buf, 1, MSG_OOB)) || $!{EINVAL} or die ref($self), " recv OOB failed: $!"; print STDERR "OOB: $buf\n" if length($buf); } } if ($self->{nonblocking}) { vec($rin, fileno(STDIN), 1) = 1; select($rin, undef, undef, undef) or die ref($self), " select read failed: $!"; } my $read = sysread(STDIN, $buf, $max && $max < $size ? $max : $size); next if !defined($read) && $!{EAGAIN}; defined($read) or die ref($self), " sysread at $len failed: $!"; if ($read == 0) { print STDERR "\n"; print STDERR "End\n"; last; } print STDERR "."; if ($max && $len + $read > $max) { $read = $max - $len; } my $off = 0; while ($off < $read) { if ($self->{nonblocking}) { vec($win, fileno(STDOUT), 1) = 1; select(undef, $win, undef, undef) or die ref($self), " select write failed: $!"; } my $write; # Unfortunately Perl installs signal handlers without # SA_RESTART. Work around by restarting manually. do { $write = syswrite(STDOUT, $buf, $read - $off, $off); } while (!defined($write) && $!{EINTR}); defined($write) || $!{ETIMEDOUT} or die ref($self), " syswrite at $len failed: $!"; defined($write) or next; $off += $write; $len += $write; } if ($max && $len == $max) { print STDERR "\n"; print STDERR "Big\n"; print STDERR "Max\n"; last; } } print STDERR "LEN: $len\n"; } sub relay_copy_datagram { my $self = shift; my $max = $self->{max}; my $idle = $self->{idle}; my $size = $self->{size} || 2**16; my $len = 0; for (my $num = 0;; $num++) { my $rin = my $win = ''; if ($idle) { vec($rin, fileno(STDIN), 1) = 1; defined(my $n = select($rin, undef, undef, $idle)) or die ref($self), " select idle failed: $!"; if ($n == 0) { print STDERR "\n"; print STDERR "Timeout\n"; last; } } elsif ($self->{nonblocking}) { vec($rin, fileno(STDIN), 1) = 1; select($rin, undef, undef, undef) or die ref($self), " select read failed: $!"; } defined(my $read = sysread(STDIN, my $buf, $size)) or die ref($self), " sysread number $num failed: $!"; print STDERR "."; if ($max && $len + $read > $max) { print STDERR "\n"; print STDERR "Max\n"; last; } if ($self->{nonblocking}) { vec($win, fileno(STDOUT), 1) = 1; select(undef, $win, undef, undef) or die ref($self), " select write failed: $!"; } defined(my $write = syswrite(STDOUT, $buf)) or die ref($self), " syswrite number $num failed: $!"; if (defined($write)) { $read == $write or die ref($self), " syswrite read $read ". "did write $write"; $len += $write; } if ($max && $len == $max) { print STDERR "\n"; print STDERR "Big\n"; print STDERR "Max\n"; last; } } print STDERR "LEN: $len\n"; } sub relay_copy { my $self = shift; my $protocol = $self->{protocol} || "tcp"; given ($protocol) { when (/tcp/) { relay_copy_stream($self, @_) } when (/udp/) { relay_copy_datagram($self, @_) } default { die ref($self), " unknown protocol name: $protocol" } } } sub relay_splice_stream { my $self = shift; my $max = $self->{max}; my $idle = $self->{idle}; my $len = 0; my $splicelen; my $shortsplice = 0; my $error; do { my $splicemax = $max ? $max - $len : 0; setsplice(\*STDIN, \*STDOUT, $splicemax, $idle) or die ref($self), " splice stdin to stdout failed: $!"; print STDERR "Spliced\n"; if ($self->{readblocking}) { my $read; # block by reading from the source socket do { # busy loop to test soreceive $read = sysread(STDIN, my $buf, 2**16); } while ($self->{nonblocking} && !defined($read) && $!{EAGAIN}); defined($read) or die ref($self), " read blocking failed: $!"; $read > 0 and die ref($self), " read blocking has data: $read"; print STDERR "Read\n"; } else { my $rin = ''; vec($rin, fileno(STDIN), 1) = 1; select($rin, undef, undef, undef) or die ref($self), " select failed: $!"; } defined($error = geterror(\*STDIN)) or die ref($self), " get error from stdin failed: $!"; ($! = $error) && ! $!{ETIMEDOUT} && ! $!{EFBIG} and die ref($self), " splice failed: $!"; defined($splicelen = getsplice(\*STDIN)) or die ref($self), " get splice len from stdin failed: $!"; print STDERR "SPLICELEN: $splicelen\n"; !$max || $splicelen <= $splicemax or die ref($self), " splice len $splicelen ". "greater than max $splicemax"; $len += $splicelen; } while ($max && $max > $len && !$shortsplice++); relay_splice_check($self, $idle, $max, $len, $error); print STDERR "LEN: $len\n"; } sub relay_splice_datagram { my $self = shift; my $max = $self->{max}; my $idle = $self->{idle}; my $splicemax = $max || 0; setsplice(\*STDIN, \*STDOUT, $splicemax, $idle) or die ref($self), " splice stdin to stdout failed: $!"; print STDERR "Spliced\n"; my $rin = ''; vec($rin, fileno(STDIN), 1) = 1; select($rin, undef, undef, undef) or die ref($self), " select failed: $!"; defined(my $error = geterror(\*STDIN)) or die ref($self), " get error from stdin failed: $!"; ($! = $error) && ! $!{ETIMEDOUT} && ! $!{EFBIG} and die ref($self), " splice failed: $!"; defined(my $splicelen = getsplice(\*STDIN)) or die ref($self), " get splice len from stdin failed: $!"; print STDERR "SPLICELEN: $splicelen\n"; !$max || $splicelen <= $splicemax or die ref($self), " splice len $splicelen ". "greater than max $splicemax"; my $len = $splicelen; if ($max && $max > $len) { defined(my $read = sysread(STDIN, my $buf, $max - $len)) or die ref($self), " sysread stdin max failed: $!"; $len += $read; } relay_splice_check($self, $idle, $max, $len, $error); print STDERR "LEN: $splicelen\n"; } sub relay_splice_check { my $self = shift; my ($idle, $max, $len, $error) = @_; if ($idle && $error == Errno::ETIMEDOUT) { print STDERR "Timeout\n"; } if ($max && $error == Errno::EFBIG) { print STDERR "Big\n"; } if ($max && $max == $len) { print STDERR "Max\n"; } elsif ($max && $max < $len) { die ref($self), " max $max less than len $len"; } elsif ($max && $max > $len && $error == Errno::EFBIG) { die ref($self), " max $max greater than len $len"; } elsif (!$error) { defined(my $read = sysread(STDIN, my $buf, 2**16)) or die ref($self), " sysread stdin failed: $!"; $read > 0 and die ref($self), " sysread stdin has data: $read"; print STDERR "End\n"; } } sub relay_splice { my $self = shift; my $protocol = $self->{protocol} || "tcp"; given ($protocol) { when (/tcp/) { relay_splice_stream($self, @_) } when (/udp/) { relay_splice_datagram($self, @_) } default { die ref($self), " unknown protocol name: $protocol" } } } sub relay { my $self = shift; my $forward = $self->{forward}; given ($forward) { when (/copy/) { relay_copy($self, @_) } when (/splice/) { relay_splice($self, @_) } default { die ref($self), " unknown forward name: $forward" } } my $soerror; $soerror = getsockopt(STDIN, SOL_SOCKET, SO_ERROR) or die ref($self), " get error from stdin failed: $!"; print STDERR "ERROR IN: ", unpack('i', $soerror), "\n"; $soerror = getsockopt(STDOUT, SOL_SOCKET, SO_ERROR) or die ref($self), " get error from stdout failed: $!"; print STDERR "ERROR OUT: ", unpack('i', $soerror), "\n"; } sub ioflip { my $self = shift; open(my $fh, '<&', \*STDIN) or die ref($self), " ioflip dup failed: $!"; open(STDIN, '<&', \*STDOUT) or die ref($self), " ioflip dup STDIN failed: $!"; open(STDOUT, '>&', $fh) or die ref($self), " ioflip dup STDOUT failed: $!"; close($fh) or die ref($self), " ioflip close failed: $!"; } sub errignore { $SIG{PIPE} = 'IGNORE'; $SIG{__DIE__} = sub { die @_ if $^S; warn "Error ignored"; my $soerror; $soerror = getsockopt(STDIN, SOL_SOCKET, SO_ERROR); print STDERR "ERROR IN: ", unpack('i', $soerror), "\n"; $soerror = getsockopt(STDOUT, SOL_SOCKET, SO_ERROR); print STDERR "ERROR OUT: ", unpack('i', $soerror), "\n"; warn @_; IO::Handle::flush(\*STDERR); POSIX::_exit(0); }; } sub shutin { my $self = shift; shutdown(\*STDIN, SHUT_RD) or die ref($self), " shutdown read failed: $!"; } sub shutout { my $self = shift; IO::Handle::flush(\*STDOUT) or die ref($self), " flush stdout failed: $!"; shutdown(\*STDOUT, SHUT_WR) or die ref($self), " shutdown write failed: $!"; } ######################################################################## # Server funcs ######################################################################## sub read_stream { my $self = shift; my $max = $self->{max}; my $ctx = Digest::MD5->new(); my $len = 0; while () { $len += length($_); $ctx->add($_); print STDERR "."; if ($max && $len >= $max) { print STDERR "\nMax"; last; } } print STDERR "\n"; print STDERR "LEN: $len\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub read_oob { my $self = shift; my $size = $self->{size} || 4091; my $ctx = Digest::MD5->new(); my $len = 0; while (1) { my $rin = my $ein = ''; vec($rin, fileno(STDIN), 1) = 1; vec($ein, fileno(STDIN), 1) = 1 unless $self->{oobinline}; select($rin, undef, $ein, undef) or die ref($self), " select failed: $!"; my $buf; my $atmark = sockatmark(\*STDIN) or die ref($self), " sockatmark failed: $!"; if ($atmark == 1) { if ($self->{oobinline}) { defined(recv(STDIN, $buf, 1, 0)) or die ref($self), " recv OOB failed: $!"; print STDERR "[$buf]"; $ctx->add("[$buf]"); $len += length($buf); } else { defined(recv(STDIN, $buf, 1, MSG_OOB)) || $!{EINVAL} or die ref($self), " recv OOB failed: $!"; print STDERR "OOB: $buf\n" if length($buf); } } defined(recv(STDIN, $buf, $size, 0)) or die ref($self), " recv failed: $!"; last unless length($buf); print STDERR $buf; $ctx->add($buf); $len += length($buf); print STDERR "."; } print STDERR "\n"; print STDERR "LEN: $len\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub read_datagram { my $self = shift; my $max = $self->{max}; my $idle = $self->{idle}; my $size = $self->{size} || 2**16; my $ctx = Digest::MD5->new(); my $len = 0; my @lengths; for (my $num = 0;; $num++) { if ($idle) { my $rin = ''; vec($rin, fileno(STDIN), 1) = 1; defined(my $n = select($rin, undef, undef, $idle)) or die ref($self), " select idle failed: $!"; if ($n == 0) { print STDERR "\n"; print STDERR "Timeout"; last; } } defined(my $read = sysread(STDIN, my $buf, $size)) or die ref($self), " sysread number $num failed: $!"; $len += $read; push @lengths, $read; $ctx->add($buf); print STDERR "."; if ($max && $len >= $max) { print STDERR "\nMax"; last; } } print STDERR "\n"; print STDERR "LEN: $len\n"; print STDERR "LENGTHS: @lengths\n"; print STDERR "MD5: ", $ctx->hexdigest, "\n"; } sub solingerin { my $self = shift; setsockopt(STDIN, SOL_SOCKET, SO_LINGER, pack('ii', 1, 0)) or die ref($self), " set linger in failed: $!"; } ######################################################################## # Script funcs ######################################################################## sub check_logs { my ($c, $r, $s, %args) = @_; return if $args{nocheck}; check_relay($c, $r, $s, %args); check_len($c, $r, $s, %args); check_lengths($c, $r, $s, %args); check_md5($c, $r, $s, %args); check_error($c, $r, $s, %args); } sub check_relay { my ($c, $r, $s, %args) = @_; return unless $r; if (defined $args{relay}{timeout}) { my $lg = $r->loggrep(qr/^Timeout$/); die "no relay timeout" if !$lg && $args{relay}{timeout}; die "relay has timeout" if $lg && !$args{relay}{timeout}; } if (defined $args{relay}{big}) { my $lg = $r->loggrep(qr/^Big$/); die "no relay big" if !$lg && $args{relay}{big}; die "relay has big" if $lg && !$args{relay}{big}; } $r->loggrep(qr/^Max$/) or die "no relay max" if $args{relay}{max} && !$args{relay}{nomax}; $r->loggrep(qr/^End$/) or die "no relay end" if $args{relay}{end}; } sub check_len { my ($c, $r, $s, %args) = @_; my ($clen, $rlen, $slen); $clen = $c->loggrep(qr/^LEN: /) // die "no client len" unless $args{client}{nocheck}; $rlen = $r->loggrep(qr/^LEN: /) // die "no relay len" if $r && ! $args{relay}{nocheck}; $slen = $s->loggrep(qr/^LEN: /) // die "no server len" unless $args{server}{nocheck}; !$clen || !$rlen || $clen eq $rlen or die "client: $clen", "relay: $rlen", "len mismatch"; !$rlen || !$slen || $rlen eq $slen or die "relay: $rlen", "server: $slen", "len mismatch"; !$clen || !$slen || $clen eq $slen or die "client: $clen", "server: $slen", "len mismatch"; !defined($args{len}) || !$clen || $clen eq "LEN: $args{len}\n" or die "client: $clen", "len $args{len} expected"; !defined($args{len}) || !$rlen || $rlen eq "LEN: $args{len}\n" or die "relay: $rlen", "len $args{len} expected"; !defined($args{len}) || !$slen || $slen eq "LEN: $args{len}\n" or die "server: $slen", "len $args{len} expected"; } sub check_lengths { my ($c, $r, $s, %args) = @_; my ($clengths, $slengths); $clengths = $c->loggrep(qr/^LENGTHS: /) unless $args{client}{nocheck}; $slengths = $s->loggrep(qr/^LENGTHS: /) unless $args{server}{nocheck}; !$clengths || !$slengths || $clengths eq $slengths or die "client: $clengths", "server: $slengths", "lengths mismatch"; !defined($args{lengths}) || !$clengths || $clengths eq "LENGTHS: $args{lengths}\n" or die "client: $clengths", "lengths $args{lengths} expected"; !defined($args{lengths}) || !$slengths || $slengths eq "LENGTHS: $args{lengths}\n" or die "server: $slengths", "lengths $args{lengths} expected"; } sub check_md5 { my ($c, $r, $s, %args) = @_; my ($cmd5, $smd5); $cmd5 = $c->loggrep(qr/^MD5: /) unless $args{client}{nocheck}; $smd5 = $s->loggrep(qr/^MD5: /) unless $args{server}{nocheck}; !$cmd5 || !$smd5 || ref($args{md5}) eq 'ARRAY' || $cmd5 eq $smd5 or die "client: $cmd5", "server: $smd5", "md5 mismatch"; my $md5 = ref($args{md5}) eq 'ARRAY' ? join('|', @{$args{md5}}) : $args{md5}; !$md5 || !$cmd5 || $cmd5 =~ /^MD5: ($md5)$/ or die "client: $cmd5", "md5 $md5 expected"; !$md5 || !$smd5 || $smd5 =~ /^MD5: ($md5)$/ or die "server: $smd5", "md5 $md5 expected"; } sub check_error { my ($c, $r, $s, %args) = @_; $args{relay}{errorin} //= 0 unless $args{relay}{nocheck}; $args{relay}{errorout} //= 0 unless $args{relay}{nocheck}; my %name2proc = (client => $c, relay => $r, server => $s); foreach my $name (qw(client relay server)) { my $p = $name2proc{$name} or next; $args{$name}{errorin} //= $args{$name}{error}; if (defined($args{$name}{errorin})) { my $ein = $p->loggrep(qr/^ERROR IN: /); defined($ein) && $ein eq "ERROR IN: $args{$name}{errorin}\n" or die "$name: $ein ", "error in $args{$name}{errorin} expected"; } if (defined($args{$name}{errorout})) { my $eout = $p->loggrep(qr/^ERROR OUT: /); defined($eout) && $eout eq "ERROR OUT: $args{$name}{errorout}\n" or die "$name: $eout ", "error out $args{$name}{errorout} expected"; } } } 1;