Fix Invalid Argument return code from IP_Factory in connect_to_server().
[openssl.git] / util / perl / TLSProxy / Proxy.pm
index 99b0dedd5beb9cc5938cc37460f8e6594e286702..a583e636cd9113d7d2d5a7a18bbda61cf459782c 100644 (file)
@@ -1,6 +1,6 @@
-# Copyright 2016 The OpenSSL Project Authors. All Rights Reserved.
+# Copyright 2016-2018 The OpenSSL Project Authors. All Rights Reserved.
 #
-# Licensed under the OpenSSL license (the "License").  You may not use
+# Licensed under the Apache License 2.0 (the "License").  You may not use
 # this file except in compliance with the License.  You can obtain a copy
 # in the file LICENSE in the source distribution or at
 # https://www.openssl.org/source/license.html
@@ -22,52 +22,16 @@ use TLSProxy::Certificate;
 use TLSProxy::CertificateVerify;
 use TLSProxy::ServerKeyExchange;
 use TLSProxy::NewSessionTicket;
-use Time::HiRes qw/usleep/;
 
-my $have_IPv6 = 0;
+my $have_IPv6;
 my $IP_factory;
 
-my $is_tls13 = 0;
-my $ciphersuite = undef;
-
-sub new
+BEGIN
 {
-    my $class = shift;
-    my ($filter,
-        $execute,
-        $cert,
-        $debug) = @_;
-
-    my $self = {
-        #Public read/write
-        proxy_addr => "localhost",
-        proxy_port => 4453,
-        server_addr => "localhost",
-        server_port => 4443,
-        filter => $filter,
-        serverflags => "",
-        clientflags => "",
-        serverconnects => 1,
-        serverpid => 0,
-        clientpid => 0,
-        reneg => 0,
-        sessionfile => undef,
-
-        #Public read
-        execute => $execute,
-        cert => $cert,
-        debug => $debug,
-        cipherc => "",
-        ciphers => "AES128-SHA:TLS13-AES-128-GCM-SHA256",
-        flight => 0,
-        record_list => [],
-        message_list => [],
-    };
-
     # IO::Socket::IP is on the core module list, IO::Socket::INET6 isn't.
     # However, IO::Socket::INET6 is older and is said to be more widely
     # deployed for the moment, and may have less bugs, so we try the latter
-    # first, then fall back on the code modules.  Worst case scenario, we
+    # first, then fall back on the core modules.  Worst case scenario, we
     # fall back to IO::Socket::INET, only supports IPv4.
     eval {
         require IO::Socket::INET6;
@@ -80,7 +44,7 @@ sub new
         $s->close();
     };
     if ($@ eq "") {
-        $IP_factory = sub { IO::Socket::INET6->new(@_); };
+        $IP_factory = sub { IO::Socket::INET6->new(Domain => AF_INET6, @_); };
         $have_IPv6 = 1;
     } else {
         eval {
@@ -98,18 +62,93 @@ sub new
             $have_IPv6 = 1;
         } else {
             $IP_factory = sub { IO::Socket::INET->new(@_); };
+            $have_IPv6 = 0;
         }
     }
+}
+
+my $is_tls13 = 0;
+my $ciphersuite = undef;
+
+sub new
+{
+    my $class = shift;
+    my ($filter,
+        $execute,
+        $cert,
+        $debug) = @_;
+
+    my $self = {
+        #Public read/write
+        proxy_addr => $have_IPv6 ? "[::1]" : "127.0.0.1",
+        filter => $filter,
+        serverflags => "",
+        clientflags => "",
+        serverconnects => 1,
+        reneg => 0,
+        sessionfile => undef,
+
+        #Public read
+        proxy_port => 0,
+        server_port => 0,
+        serverpid => 0,
+        clientpid => 0,
+        execute => $execute,
+        cert => $cert,
+        debug => $debug,
+        cipherc => "",
+        ciphersuitesc => "",
+        ciphers => "AES128-SHA",
+        ciphersuitess => "TLS_AES_128_GCM_SHA256",
+        flight => -1,
+        direction => -1,
+        partial => ["", ""],
+        record_list => [],
+        message_list => [],
+    };
+
+    # Create the Proxy socket
+    my $proxaddr = $self->{proxy_addr};
+    $proxaddr =~ s/[\[\]]//g; # Remove [ and ]
+    my @proxyargs = (
+        LocalHost   => $proxaddr,
+        LocalPort   => 0,
+        Proto       => "tcp",
+        Listen      => SOMAXCONN,
+       );
+
+    if (my $sock = $IP_factory->(@proxyargs)) {
+        $self->{proxy_sock} = $sock;
+        $self->{proxy_port} = $sock->sockport();
+        $self->{proxy_addr} = $sock->sockhost();
+        $self->{proxy_addr} =~ s/(.*:.*)/[$1]/;
+        print "Proxy started on port ",
+              "$self->{proxy_addr}:$self->{proxy_port}\n";
+        # use same address for s_server
+        $self->{server_addr} = $self->{proxy_addr};
+    } else {
+        warn "Failed creating proxy socket (".$proxaddr.",0): $!\n";
+    }
 
     return bless $self, $class;
 }
 
+sub DESTROY
+{
+    my $self = shift;
+
+    $self->{proxy_sock}->close() if $self->{proxy_sock};
+}
+
 sub clearClient
 {
     my $self = shift;
 
     $self->{cipherc} = "";
-    $self->{flight} = 0;
+    $self->{ciphersuitec} = "";
+    $self->{flight} = -1;
+    $self->{direction} = -1;
+    $self->{partial} = ["", ""];
     $self->{record_list} = [];
     $self->{message_list} = [];
     $self->{clientflags} = "";
@@ -127,7 +166,8 @@ sub clear
     my $self = shift;
 
     $self->clearClient;
-    $self->{ciphers} = "AES128-SHA:TLS13-AES-128-GCM-SHA256";
+    $self->{ciphers} = "AES128-SHA";
+    $self->{ciphersuitess} = "TLS_AES_128_GCM_SHA256";
     $self->{serverflags} = "";
     $self->{serverconnects} = 1;
     $self->{serverpid} = 0;
@@ -150,35 +190,118 @@ sub clientrestart
     $self->clientstart;
 }
 
+sub connect_to_server
+{
+    my $self = shift;
+    my $servaddr = $self->{server_addr};
+
+    $servaddr =~ s/[\[\]]//g; # Remove [ and ]
+
+    my $sock = $IP_factory->(PeerAddr => $servaddr,
+                             PeerPort => $self->{server_port},
+                             Proto => 'tcp');
+    if (!defined($sock)) {
+        my $err = $!;
+        kill(3, $self->{real_serverpid});
+        die "unable to connect: $err\n";
+    }
+
+    $self->{server_sock} = $sock;
+}
+
 sub start
 {
     my ($self) = shift;
     my $pid;
 
-    $pid = fork();
-    if ($pid == 0) {
-        if (!$self->debug) {
-            open(STDOUT, ">", File::Spec->devnull())
-                or die "Failed to redirect stdout: $!";
-            open(STDERR, ">&STDOUT");
-        }
-        my $execcmd = $self->execute
-            ." s_server -no_comp -rev -engine ossltest -accept "
-            .($self->server_port)
-            ." -cert ".$self->cert." -cert2 ".$self->cert
-            ." -naccept ".$self->serverconnects;
-        if ($self->ciphers ne "") {
-            $execcmd .= " -cipher ".$self->ciphers;
-        }
-        if ($self->serverflags ne "") {
-            $execcmd .= " ".$self->serverflags;
+    if ($self->{proxy_sock} == 0) {
+        return 0;
+    }
+
+    my $execcmd = $self->execute
+        ." s_server -max_protocol TLSv1.3 -no_comp -rev -engine ossltest"
+        #In TLSv1.3 we issue two session tickets. The default session id
+        #callback gets confused because the ossltest engine causes the same
+        #session id to be created twice due to the changed random number
+        #generation. Using "-ext_cache" replaces the default callback with a
+        #different one that doesn't get confused.
+        ." -ext_cache"
+        ." -accept $self->{server_addr}:0"
+        ." -cert ".$self->cert." -cert2 ".$self->cert
+        ." -naccept ".$self->serverconnects;
+    if ($self->ciphers ne "") {
+        $execcmd .= " -cipher ".$self->ciphers;
+    }
+    if ($self->ciphersuitess ne "") {
+        $execcmd .= " -ciphersuites ".$self->ciphersuitess;
+    }
+    if ($self->serverflags ne "") {
+        $execcmd .= " ".$self->serverflags;
+    }
+    if ($self->debug) {
+        print STDERR "Server command: $execcmd\n";
+    }
+
+    open(my $savedin, "<&STDIN");
+
+    # Temporarily replace STDIN so that sink process can inherit it...
+    $pid = open(STDIN, "$execcmd 2>&1 |") or die "Failed to $execcmd: $!\n";
+    $self->{real_serverpid} = $pid;
+
+    # Process the output from s_server until we find the ACCEPT line, which
+    # tells us what the accepting address and port are.
+    while (<>) {
+        print;
+        s/\R$//;                # Better chomp
+        next unless (/^ACCEPT\s.*:(\d+)$/);
+        $self->{server_port} = $1;
+        last;
+    }
+
+    if ($self->{server_port} == 0) {
+        # This actually means that s_server exited, because otherwise
+        # we would still searching for ACCEPT...
+        waitpid($pid, 0);
+        die "no ACCEPT detected in '$execcmd' output: $?\n";
+    }
+
+    # Just make sure everything else is simply printed [as separate lines].
+    # The sub process simply inherits our STD* and will keep consuming
+    # server's output and printing it as long as there is anything there,
+    # out of our way.
+    my $error;
+    $pid = undef;
+    if (eval { require Win32::Process; 1; }) {
+        if (Win32::Process::Create(my $h, $^X, "perl -ne print", 0, 0, ".")) {
+            $pid = $h->GetProcessID();
+            $self->{proc_handle} = $h;  # hold handle till next round [or exit]
+        } else {
+            $error = Win32::FormatMessage(Win32::GetLastError());
         }
-        if ($self->debug) {
-            print STDERR "Server command: $execcmd\n";
+    } else {
+        if (defined($pid = fork)) {
+            $pid or exec("$^X -ne print") or exit($!);
+        } else {
+            $error = $!;
         }
-        exec($execcmd);
     }
-    $self->serverpid($pid);
+
+    # Change back to original stdin
+    open(STDIN, "<&", $savedin);
+    close($savedin);
+
+    if (!defined($pid)) {
+        kill(3, $self->{real_serverpid});
+        die "Failed to capture s_server's output: $error\n";
+    }
+
+    $self->{serverpid} = $pid;
+
+    print STDERR "Server responds on ",
+                 "$self->{server_addr}:$self->{server_port}\n";
+
+    # Connect right away...
+    $self->connect_to_server();
 
     return $self->clientstart;
 }
@@ -186,170 +309,155 @@ sub start
 sub clientstart
 {
     my ($self) = shift;
-    my $oldstdout;
 
-    if(!$self->debug) {
-        open DEVNULL, ">", File::Spec->devnull();
-        $oldstdout = select(DEVNULL);
-    }
+    if ($self->execute) {
+        my $pid;
+        my $execcmd = $self->execute
+             ." s_client -max_protocol TLSv1.3 -engine ossltest"
+             ." -connect $self->{proxy_addr}:$self->{proxy_port}";
+        if ($self->cipherc ne "") {
+            $execcmd .= " -cipher ".$self->cipherc;
+        }
+        if ($self->ciphersuitesc ne "") {
+            $execcmd .= " -ciphersuites ".$self->ciphersuitesc;
+        }
+        if ($self->clientflags ne "") {
+            $execcmd .= " ".$self->clientflags;
+        }
+        if ($self->clientflags !~ m/-(no)?servername/) {
+            $execcmd .= " -servername localhost";
+        }
+        if (defined $self->sessionfile) {
+            $execcmd .= " -ign_eof";
+        }
+        if ($self->debug) {
+            print STDERR "Client command: $execcmd\n";
+        }
 
-    # Create the Proxy socket
-    my $proxaddr = $self->proxy_addr;
-    $proxaddr =~ s/[\[\]]//g; # Remove [ and ]
-    my $proxy_sock = $IP_factory->(
-        LocalHost   => $proxaddr,
-        LocalPort   => $self->proxy_port,
-        Proto       => "tcp",
-        Listen      => SOMAXCONN,
-        ReuseAddr   => 1
-    );
+        open(my $savedout, ">&STDOUT");
+        # If we open pipe with new descriptor, attempt to close it,
+        # explicitly or implicitly, would incur waitpid and effectively
+        # dead-lock...
+        if (!($pid = open(STDOUT, "| $execcmd"))) {
+            my $err = $!;
+            kill(3, $self->{real_serverpid});
+            die "Failed to $execcmd: $err\n";
+        }
+        $self->{clientpid} = $pid;
 
-    if ($proxy_sock) {
-        print "Proxy started on port ".$self->proxy_port."\n";
-    } else {
-        warn "Failed creating proxy socket (".$proxaddr.",".$self->proxy_port."): $!\n";
-        return 0;
-    }
+        # queue [magic] input
+        print $self->reneg ? "R" : "test";
 
-    if ($self->execute) {
-        my $pid = fork();
-        if ($pid == 0) {
-            if (!$self->debug) {
-                open(STDOUT, ">", File::Spec->devnull())
-                    or die "Failed to redirect stdout: $!";
-                open(STDERR, ">&STDOUT");
-            }
-            my $echostr;
-            if ($self->reneg()) {
-                $echostr = "R";
-            } else {
-                $echostr = "test";
-            }
-            my $execcmd = "echo ".$echostr." | ".$self->execute
-                 ." s_client -engine ossltest -connect "
-                 .($self->proxy_addr).":".($self->proxy_port);
-            if ($self->cipherc ne "") {
-                $execcmd .= " -cipher ".$self->cipherc;
-            }
-            if ($self->clientflags ne "") {
-                $execcmd .= " ".$self->clientflags;
-            }
-            if (defined $self->sessionfile) {
-                $execcmd .= " -ign_eof";
-            }
-            if ($self->debug) {
-                print STDERR "Client command: $execcmd\n";
-            }
-            exec($execcmd);
-        }
-        $self->clientpid($pid);
+        # this closes client's stdin without waiting for its pid
+        open(STDOUT, ">&", $savedout);
+        close($savedout);
     }
 
     # Wait for incoming connection from client
+    my $fdset = IO::Select->new($self->{proxy_sock});
+    if (!$fdset->can_read(60)) {
+        kill(3, $self->{real_serverpid});
+        die "s_client didn't try to connect\n";
+    }
+
     my $client_sock;
-    if(!($client_sock = $proxy_sock->accept())) {
+    if(!($client_sock = $self->{proxy_sock}->accept())) {
         warn "Failed accepting incoming connection: $!\n";
         return 0;
     }
 
     print "Connection opened\n";
 
-    # Now connect to the server
-    my $retry = 10;
-    my $server_sock;
-    #We loop over this a few times because sometimes s_server can take a while
-    #to start up
-    do {
-        my $servaddr = $self->server_addr;
-        $servaddr =~ s/[\[\]]//g; # Remove [ and ]
-        eval {
-            $server_sock = $IP_factory->(
-                PeerAddr => $servaddr,
-                PeerPort => $self->server_port,
-                MultiHomed => 1,
-                Proto => 'tcp'
-            );
-        };
-
-        $retry--;
-        #Some buggy IP factories can return a defined server_sock that hasn't
-        #actually connected, so we check peerport too
-        if ($@ || !defined($server_sock) || !defined($server_sock->peerport)) {
-            $server_sock->close() if defined($server_sock);
-            undef $server_sock;
-            if ($retry) {
-                #Sleep for a short while
-                select(undef, undef, undef, 0.1);
-            } else {
-                warn "Failed to start up server (".$servaddr.",".$self->server_port."): $!\n";
-                return 0;
-            }
-        }
-    } while (!$server_sock);
-
-    my $sel = IO::Select->new($server_sock, $client_sock);
+    my $server_sock = $self->{server_sock};
     my $indata;
-    my @handles = ($server_sock, $client_sock);
 
     #Wait for either the server socket or the client socket to become readable
+    $fdset = IO::Select->new($server_sock, $client_sock);
     my @ready;
     my $ctr = 0;
-    while(     (!(TLSProxy::Message->end)
-                || (defined $self->sessionfile()
-                    && (-s $self->sessionfile()) == 0))
-            && $ctr < 10) {
-        if (!(@ready = $sel->can_read(1))) {
+    local $SIG{PIPE} = "IGNORE";
+    $self->{saw_session_ticket} = undef;
+    while($fdset->count && $ctr < 10) {
+        if (defined($self->{sessionfile})) {
+            # s_client got -ign_eof and won't be exiting voluntarily, so we
+            # look for data *and* session ticket...
+            last if TLSProxy::Message->success()
+                    && $self->{saw_session_ticket};
+        }
+        if (!(@ready = $fdset->can_read(1))) {
             $ctr++;
             next;
         }
         foreach my $hand (@ready) {
             if ($hand == $server_sock) {
-                $server_sock->sysread($indata, 16384) or goto END;
-                $indata = $self->process_packet(1, $indata);
-                $client_sock->syswrite($indata);
-                $ctr = 0;
+                if ($server_sock->sysread($indata, 16384)) {
+                    if ($indata = $self->process_packet(1, $indata)) {
+                        $client_sock->syswrite($indata) or goto END;
+                    }
+                    $ctr = 0;
+                } else {
+                    $fdset->remove($server_sock);
+                    $client_sock->shutdown(SHUT_WR);
+                }
             } elsif ($hand == $client_sock) {
-                $client_sock->sysread($indata, 16384) or goto END;
-                $indata = $self->process_packet(0, $indata);
-                $server_sock->syswrite($indata);
-                $ctr = 0;
+                if ($client_sock->sysread($indata, 16384)) {
+                    if ($indata = $self->process_packet(0, $indata)) {
+                        $server_sock->syswrite($indata) or goto END;
+                    }
+                    $ctr = 0;
+                } else {
+                    $fdset->remove($client_sock);
+                    $server_sock->shutdown(SHUT_WR);
+                }
             } else {
+                kill(3, $self->{real_serverpid});
                 die "Unexpected handle";
             }
         }
     }
 
-    die "No progress made" if $ctr >= 10;
+    if ($ctr >= 10) {
+        kill(3, $self->{real_serverpid});
+        die "No progress made";
+    }
 
     END:
     print "Connection closed\n";
     if($server_sock) {
         $server_sock->close();
+        $self->{server_sock} = undef;
     }
     if($client_sock) {
         #Closing this also kills the child process
         $client_sock->close();
     }
-    if($proxy_sock) {
-        $proxy_sock->close();
-    }
-    if(!$self->debug) {
-        select($oldstdout);
-    }
-    $self->serverconnects($self->serverconnects - 1);
-    if ($self->serverconnects == 0) {
-        die "serverpid is zero\n" if $self->serverpid == 0;
-        print "Waiting for server process to close: "
-              .$self->serverpid."\n";
-        waitpid( $self->serverpid, 0);
-        die "exit code $? from server process\n" if $? != 0;
+
+    my $pid;
+    if (--$self->{serverconnects} == 0) {
+        $pid = $self->{serverpid};
+        print "Waiting for 'perl -ne print' process to close: $pid...\n";
+        $pid = waitpid($pid, 0);
+        if ($pid > 0) {
+            die "exit code $? from 'perl -ne print' process\n" if $? != 0;
+        } elsif ($pid == 0) {
+            kill(3, $self->{real_serverpid});
+            die "lost control over $self->{serverpid}?";
+        }
+        $pid = $self->{real_serverpid};
+        print "Waiting for s_server process to close: $pid...\n";
+        # it's done already, just collect the exit code [and reap]...
+        waitpid($pid, 0);
+        die "exit code $? from s_server process\n" if $? != 0;
     } else {
-        # Give s_server sufficient time to finish what it was doing
-        usleep(250000);
+        # It's a bit counter-intuitive spot to make next connection to
+        # the s_server. Rationale is that established connection works
+        # as syncronization point, in sense that this way we know that
+        # s_server is actually done with current session...
+        $self->connect_to_server();
     }
-    die "clientpid is zero\n" if $self->clientpid == 0;
-    print "Waiting for client process to close: ".$self->clientpid."\n";
-    waitpid($self->clientpid, 0);
+    $pid = $self->{clientpid};
+    print "Waiting for s_client process to close: $pid...\n";
+    waitpid($pid, 0);
 
     return 1;
 }
@@ -368,34 +476,47 @@ sub process_packet
         print "Received client packet\n";
     }
 
+    if ($self->{direction} != $server) {
+        $self->{flight} = $self->{flight} + 1;
+        $self->{direction} = $server;
+    }
+
     print "Packet length = ".length($packet)."\n";
     print "Processing flight ".$self->flight."\n";
 
     #Return contains the list of record found in the packet followed by the
-    #list of messages in those records
-    my @ret = TLSProxy::Record->get_records($server, $self->flight, $packet);
-    push @{$self->record_list}, @{$ret[0]};
+    #list of messages in those records and any partial message
+    my @ret = TLSProxy::Record->get_records($server, $self->flight,
+                                            $self->{partial}[$server].$packet);
+    $self->{partial}[$server] = $ret[2];
+    push @{$self->{record_list}}, @{$ret[0]};
     push @{$self->{message_list}}, @{$ret[1]};
 
     print "\n";
 
+    if (scalar(@{$ret[0]}) == 0 or length($ret[2]) != 0) {
+        return "";
+    }
+
     #Finished parsing. Call user provided filter here
-    if(defined $self->filter) {
+    if (defined $self->filter) {
         $self->filter->($self);
     }
 
+    #Take a note on NewSessionTicket
+    foreach my $message (reverse @{$self->{message_list}}) {
+        if ($message->{mt} == TLSProxy::Message::MT_NEW_SESSION_TICKET) {
+            $self->{saw_session_ticket} = 1;
+            last;
+        }
+    }
+
     #Reconstruct the packet
     $packet = "";
     foreach my $record (@{$self->record_list}) {
-        #We only replay the records for the current flight
-        if ($record->flight != $self->flight) {
-            next;
-        }
         $packet .= $record->reconstruct_record($server);
     }
 
-    $self->{flight} = $self->{flight} + 1;
-
     print "Forwarded packet length = ".length($packet)."\n\n";
 
     return $packet;
@@ -442,40 +563,38 @@ sub supports_IPv6
     my $self = shift;
     return $have_IPv6;
 }
-
-#Read/write accessors
 sub proxy_addr
 {
     my $self = shift;
-    if (@_) {
-        $self->{proxy_addr} = shift;
-    }
     return $self->{proxy_addr};
 }
 sub proxy_port
 {
     my $self = shift;
-    if (@_) {
-        $self->{proxy_port} = shift;
-    }
     return $self->{proxy_port};
 }
 sub server_addr
 {
     my $self = shift;
-    if (@_) {
-        $self->{server_addr} = shift;
-    }
     return $self->{server_addr};
 }
 sub server_port
 {
     my $self = shift;
-    if (@_) {
-        $self->{server_port} = shift;
-    }
     return $self->{server_port};
 }
+sub serverpid
+{
+    my $self = shift;
+    return $self->{serverpid};
+}
+sub clientpid
+{
+    my $self = shift;
+    return $self->{clientpid};
+}
+
+#Read/write accessors
 sub filter
 {
     my $self = shift;
@@ -492,6 +611,14 @@ sub cipherc
     }
     return $self->{cipherc};
 }
+sub ciphersuitesc
+{
+    my $self = shift;
+    if (@_) {
+        $self->{ciphersuitesc} = shift;
+    }
+    return $self->{ciphersuitesc};
+}
 sub ciphers
 {
     my $self = shift;
@@ -500,6 +627,14 @@ sub ciphers
     }
     return $self->{ciphers};
 }
+sub ciphersuitess
+{
+    my $self = shift;
+    if (@_) {
+        $self->{ciphersuitess} = shift;
+    }
+    return $self->{ciphersuitess};
+}
 sub serverflags
 {
     my $self = shift;
@@ -536,22 +671,6 @@ sub message_list
     }
     return $self->{message_list};
 }
-sub serverpid
-{
-    my $self = shift;
-    if (@_) {
-        $self->{serverpid} = shift;
-    }
-    return $self->{serverpid};
-}
-sub clientpid
-{
-    my $self = shift;
-    if (@_) {
-        $self->{clientpid} = shift;
-    }
-    return $self->{clientpid};
-}
 
 sub fill_known_data
 {