xref: /openssl/util/perl/TLSProxy/Proxy.pm (revision 3d3bb26a)
1# Copyright 2016-2024 The OpenSSL Project Authors. All Rights Reserved.
2#
3# Licensed under the Apache License 2.0 (the "License").  You may not use
4# this file except in compliance with the License.  You can obtain a copy
5# in the file LICENSE in the source distribution or at
6# https://www.openssl.org/source/license.html
7
8use strict;
9use POSIX ":sys_wait_h";
10use IPC::Open2;
11
12package TLSProxy::Proxy;
13
14use File::Spec;
15use IO::Socket;
16use IO::Select;
17use TLSProxy::Record;
18use TLSProxy::Message;
19use TLSProxy::ClientHello;
20use TLSProxy::ServerHello;
21use TLSProxy::HelloVerifyRequest;
22use TLSProxy::EncryptedExtensions;
23use TLSProxy::Certificate;
24use TLSProxy::CertificateRequest;
25use TLSProxy::CertificateVerify;
26use TLSProxy::ServerKeyExchange;
27use TLSProxy::NewSessionTicket;
28use TLSProxy::NextProto;
29
30my $have_IPv6;
31my $IP_factory;
32
33BEGIN
34{
35    # IO::Socket::IP is on the core module list, IO::Socket::INET6 isn't.
36    # However, IO::Socket::INET6 is older and is said to be more widely
37    # deployed for the moment, and may have less bugs, so we try the latter
38    # first, then fall back on the core modules.  Worst case scenario, we
39    # fall back to IO::Socket::INET, only supports IPv4.
40    eval {
41        require IO::Socket::INET6;
42        my $s = IO::Socket::INET6->new(
43            LocalAddr => "::1",
44            LocalPort => 0,
45            Listen=>1,
46            );
47        $s or die "\n";
48        $s->close();
49    };
50    if ($@ eq "") {
51        $IP_factory = sub { IO::Socket::INET6->new(Domain => AF_INET6, @_); };
52        $have_IPv6 = 1;
53    } else {
54        eval {
55            require IO::Socket::IP;
56            my $s = IO::Socket::IP->new(
57                LocalAddr => "::1",
58                LocalPort => 0,
59                Listen=>1,
60                );
61            $s or die "\n";
62            $s->close();
63        };
64        if ($@ eq "") {
65            $IP_factory = sub { IO::Socket::IP->new(@_); };
66            $have_IPv6 = 1;
67        } else {
68            $IP_factory = sub { IO::Socket::INET->new(@_); };
69            $have_IPv6 = 0;
70        }
71    }
72}
73
74my $is_tls13 = 0;
75my $ciphersuite = undef;
76
77sub new {
78    my $class = shift;
79    my ($filter,
80        $execute,
81        $cert,
82        $debug) = @_;
83    return init($class, $filter, $execute, $cert, $debug, 0);
84}
85
86sub new_dtls {
87    my $class = shift;
88    my ($filter,
89        $execute,
90        $cert,
91        $debug) = @_;
92    return init($class, $filter, $execute, $cert, $debug, 1);
93}
94
95sub init
96{
97    my $class = shift;
98    my ($filter,
99        $execute,
100        $cert,
101        $debug,
102        $isdtls) = @_;
103
104    my $self = {
105        #Public read/write
106        proxy_addr => $have_IPv6 ? "[::1]" : "127.0.0.1",
107        client_addr => $have_IPv6 ? "[::1]" : "127.0.0.1",
108        filter => $filter,
109        serverflags => "",
110        clientflags => "",
111        serverconnects => 1,
112        reneg => 0,
113        sessionfile => undef,
114
115        #Public read
116        isdtls => $isdtls,
117        proxy_port => 0,
118        client_port => 49152 + int(rand(65535 - 49152)),
119        server_port => 0,
120        serverpid => 0,
121        clientpid => 0,
122        execute => $execute,
123        cert => $cert,
124        debug => $debug,
125        cipherc => "",
126        ciphersuitesc => "",
127        ciphers => "AES128-SHA",
128        ciphersuitess => "TLS_AES_128_GCM_SHA256",
129        flight => -1,
130        direction => -1,
131        partial => ["", ""],
132        record_list => [],
133        message_list => [],
134    };
135
136    return bless $self, $class;
137}
138
139sub DESTROY
140{
141    my $self = shift;
142
143    $self->{proxy_sock}->close() if $self->{proxy_sock};
144}
145
146sub clearClient
147{
148    my $self = shift;
149
150    $self->{cipherc} = "";
151    $self->{ciphersuitec} = "";
152    $self->{flight} = -1;
153    $self->{direction} = -1;
154    $self->{partial} = ["", ""];
155    $self->{record_list} = [];
156    $self->{message_list} = [];
157    $self->{clientflags} = "";
158    $self->{sessionfile} = undef;
159    $self->{clientpid} = 0;
160    $is_tls13 = 0;
161    $ciphersuite = undef;
162
163    TLSProxy::Message->clear();
164    TLSProxy::Record->clear();
165}
166
167sub clear
168{
169    my $self = shift;
170
171    $self->clearClient;
172    $self->{ciphers} = "AES128-SHA";
173    $self->{ciphersuitess} = "TLS_AES_128_GCM_SHA256";
174    $self->{serverflags} = "";
175    $self->{serverconnects} = 1;
176    $self->{serverpid} = 0;
177    $self->{reneg} = 0;
178}
179
180sub restart
181{
182    my $self = shift;
183
184    $self->clear;
185    $self->start;
186}
187
188sub clientrestart
189{
190    my $self = shift;
191
192    $self->clear;
193    $self->clientstart;
194}
195
196sub connect_to_server
197{
198    my $self = shift;
199    my $servaddr = $self->{server_addr};
200
201    $servaddr =~ s/[\[\]]//g; # Remove [ and ]
202
203    my $sock = $IP_factory->(PeerAddr => $servaddr,
204                             PeerPort => $self->{server_port},
205                             Proto => $self->{isdtls} ? 'udp' : 'tcp');
206    if (!defined($sock)) {
207        my $err = $!;
208        kill(3, $self->{serverpid});
209        die "unable to connect: $err\n";
210    }
211
212    $self->{server_sock} = $sock;
213}
214
215sub start
216{
217    my ($self) = shift;
218    my $pid;
219
220    # Create the Proxy socket
221    my $proxaddr = $self->{proxy_addr};
222    $proxaddr =~ s/[\[\]]//g; # Remove [ and ]
223    my $clientaddr = $self->{client_addr};
224    $clientaddr =~ s/[\[\]]//g; # Remove [ and ]
225
226    my @proxyargs;
227
228    if ($self->{isdtls}) {
229        @proxyargs = (
230            LocalHost   => $proxaddr,
231            LocalPort   => 0,
232            PeerHost   => $clientaddr,
233            PeerPort   => $self->{client_port},
234            Proto       => "udp",
235        );
236    } else {
237        @proxyargs = (
238            LocalHost   => $proxaddr,
239            LocalPort   => 0,
240            Proto       => "tcp",
241            Listen      => SOMAXCONN,
242        );
243    }
244
245    if (my $sock = $IP_factory->(@proxyargs)) {
246        $self->{proxy_sock} = $sock;
247        $self->{proxy_port} = $sock->sockport();
248        $self->{proxy_addr} = $sock->sockhost();
249        $self->{proxy_addr} =~ s/(.*:.*)/[$1]/;
250        print "Proxy started on port ",
251            "$self->{proxy_addr}:$self->{proxy_port}\n";
252        # use same address for s_server
253        $self->{server_addr} = $self->{proxy_addr};
254    } else {
255        warn "Failed creating proxy socket (".$proxaddr.",0): $!\n";
256    }
257
258    if ($self->{proxy_sock} == 0) {
259        return 0;
260    }
261
262    my $execcmd = $self->execute
263        ." s_server -no_comp -engine ossltest -state"
264        #In TLSv1.3 we issue two session tickets. The default session id
265        #callback gets confused because the ossltest engine causes the same
266        #session id to be created twice due to the changed random number
267        #generation. Using "-ext_cache" replaces the default callback with a
268        #different one that doesn't get confused.
269        ." -ext_cache"
270        ." -accept $self->{server_addr}:0"
271        ." -cert ".$self->cert." -cert2 ".$self->cert
272        ." -naccept ".$self->serverconnects;
273    if ($self->{isdtls}) {
274        $execcmd .= " -dtls -max_protocol DTLSv1.2"
275                    # TLSProxy does not support message fragmentation. So
276                    # set a high mtu and fingers crossed.
277                    ." -mtu 1500";
278    } else {
279        $execcmd .= " -rev -max_protocol TLSv1.3";
280    }
281    if ($self->ciphers ne "") {
282        $execcmd .= " -cipher ".$self->ciphers;
283    }
284    if ($self->ciphersuitess ne "") {
285        $execcmd .= " -ciphersuites ".$self->ciphersuitess;
286    }
287    if ($self->serverflags ne "") {
288        $execcmd .= " ".$self->serverflags;
289    }
290    if ($self->debug) {
291        print STDERR "Server command: $execcmd\n";
292    }
293    my $sin = undef;
294    my $sout = undef;
295    if ("$^O" eq "MSWin32") {
296        $pid = IPC::Open2::open2($sout, $sin, $execcmd) or die "Failed to $execcmd: $!\n";
297    } else {
298        $pid = IPC::Open3::open3($sin, $sout, undef, $execcmd) or die "Failed to $execcmd: $!\n";
299    }
300
301    $self->{serverpid} = $pid;
302
303    # Process the output from s_server until we find the ACCEPT line, which
304    # tells us what the accepting address and port are.
305    while (<$sout>) {
306        print;
307        s/\R$//; # chomp does not work on windows.
308        next unless (/^ACCEPT\s.*:(\d+)$/);
309        $self->{server_port} = $1;
310        last;
311    }
312
313    if ($self->{server_port} == 0) {
314        # This actually means that s_server exited, because otherwise
315        # we would still searching for ACCEPT...
316        waitpid($pid, 0);
317        die "no ACCEPT detected in '$execcmd' output: $?\n";
318    }
319
320    print STDERR "Server responds on ",
321                 "$self->{server_addr}:$self->{server_port}\n";
322
323    # Connect right away...
324    $self->connect_to_server();
325
326    return $self->clientstart;
327}
328
329sub clientstart
330{
331    my ($self) = shift;
332
333    my $success = 1;
334
335    if ($self->execute) {
336        my $pid;
337        my $execcmd = $self->execute
338             ." s_client -engine ossltest"
339             ." -connect $self->{proxy_addr}:$self->{proxy_port}";
340        if ($self->{isdtls}) {
341            $execcmd .= " -dtls -max_protocol DTLSv1.2"
342                        # TLSProxy does not support message fragmentation. So
343                        # set a high mtu and fingers crossed.
344                        ." -mtu 1500"
345                        # UDP has no "accept" for sockets which means we need to
346                        # know were to send data back to.
347                        ." -bind $self->{client_addr}:$self->{client_port}";
348        } else {
349            $execcmd .= " -max_protocol TLSv1.3";
350        }
351        if ($self->cipherc ne "") {
352            $execcmd .= " -cipher ".$self->cipherc;
353        }
354        if ($self->ciphersuitesc ne "") {
355            $execcmd .= " -ciphersuites ".$self->ciphersuitesc;
356        }
357        if ($self->clientflags ne "") {
358            $execcmd .= " ".$self->clientflags;
359        }
360        if ($self->clientflags !~ m/-(no)?servername/) {
361            $execcmd .= " -servername localhost";
362        }
363        if (defined $self->sessionfile) {
364            $execcmd .= " -ign_eof";
365        }
366        if ($self->debug) {
367            print STDERR "Client command: $execcmd\n";
368        }
369
370        open(my $savedout, ">&STDOUT");
371        # If we open pipe with new descriptor, attempt to close it,
372        # explicitly or implicitly, would incur waitpid and effectively
373        # dead-lock...
374        if (!($pid = open(STDOUT, "| $execcmd"))) {
375            my $err = $!;
376            kill(3, $self->{serverpid});
377            die "Failed to $execcmd: $err\n";
378        }
379        $self->{clientpid} = $pid;
380
381        # queue [magic] input
382        print $self->reneg ? "R" : "test";
383
384        # this closes client's stdin without waiting for its pid
385        open(STDOUT, ">&", $savedout);
386        close($savedout);
387    }
388
389    # Wait for incoming connection from client
390    my $fdset = IO::Select->new($self->{proxy_sock});
391    if (!$fdset->can_read(60)) {
392        kill(3, $self->{serverpid});
393        die "s_client didn't try to connect\n";
394    }
395
396    my $client_sock;
397    if($self->{isdtls}) {
398        $client_sock = $self->{proxy_sock}
399    } elsif (!($client_sock = $self->{proxy_sock}->accept())) {
400        warn "Failed accepting incoming connection: $!\n";
401        return 0;
402    }
403
404    print "Connection opened\n";
405
406    my $server_sock = $self->{server_sock};
407    my $indata;
408
409    #Wait for either the server socket or the client socket to become readable
410    $fdset = IO::Select->new($server_sock, $client_sock);
411    my @ready;
412    my $ctr = 0;
413    local $SIG{PIPE} = "IGNORE";
414    $self->{saw_session_ticket} = undef;
415    while($fdset->count && $ctr < 10) {
416        if (defined($self->{sessionfile})) {
417            # s_client got -ign_eof and won't be exiting voluntarily, so we
418            # look for data *and* session ticket...
419            last if TLSProxy::Message->success()
420                    && $self->{saw_session_ticket};
421        }
422        if (!(@ready = $fdset->can_read(1))) {
423            last if TLSProxy::Message->success()
424                && $self->{saw_session_ticket};
425
426            $ctr++;
427            next;
428        }
429        foreach my $hand (@ready) {
430            if ($hand == $server_sock) {
431                if ($server_sock->sysread($indata, 16384)) {
432                    if ($indata = $self->process_packet(1, $indata)) {
433                        $client_sock->syswrite($indata) or goto END;
434                    }
435                    $ctr = 0;
436                } else {
437                    $fdset->remove($server_sock);
438                    $client_sock->shutdown(SHUT_WR);
439                }
440            } elsif ($hand == $client_sock) {
441                if ($client_sock->sysread($indata, 16384)) {
442                    if ($indata = $self->process_packet(0, $indata)) {
443                        $server_sock->syswrite($indata) or goto END;
444                    }
445                    $ctr = 0;
446                } else {
447                    $fdset->remove($client_sock);
448                    $server_sock->shutdown(SHUT_WR);
449                }
450            } else {
451                kill(3, $self->{serverpid});
452                die "Unexpected handle";
453            }
454        }
455    }
456
457    if ($ctr >= 10) {
458        kill(3, $self->{serverpid});
459        print "No progress made\n";
460        $success = 0;
461    }
462
463    END:
464    print "Connection closed\n";
465    if($server_sock) {
466        $server_sock->close();
467        $self->{server_sock} = undef;
468    }
469    if($client_sock) {
470        #Closing this also kills the child process
471        $client_sock->close();
472    }
473
474    my $pid;
475    if (--$self->{serverconnects} == 0) {
476        $pid = $self->{serverpid};
477        print "Waiting for s_server process to close: $pid...\n";
478        # it's done already, just collect the exit code [and reap]...
479        waitpid($pid, 0);
480        die "exit code $? from s_server process\n" if $? != 0;
481    } else {
482        # It's a bit counter-intuitive spot to make next connection to
483        # the s_server. Rationale is that established connection works
484        # as synchronization point, in sense that this way we know that
485        # s_server is actually done with current session...
486        $self->connect_to_server();
487    }
488    $pid = $self->{clientpid};
489    print "Waiting for s_client process to close: $pid...\n";
490    waitpid($pid, 0);
491
492    return $success;
493}
494
495sub process_packet
496{
497    my ($self, $server, $packet) = @_;
498    my $len_real;
499    my $decrypt_len;
500    my $data;
501    my $recnum;
502
503    if ($server) {
504        print "Received server packet\n";
505    } else {
506        print "Received client packet\n";
507    }
508
509    if ($self->{direction} != $server) {
510        $self->{flight} = $self->{flight} + 1;
511        $self->{direction} = $server;
512    }
513
514    print "Packet length = ".length($packet)."\n";
515    print "Processing flight ".$self->flight."\n";
516
517    #Return contains the list of record found in the packet followed by the
518    #list of messages in those records and any partial message
519    my @ret = TLSProxy::Record->get_records($server, $self->flight,
520                                            $self->{partial}[$server].$packet,
521                                            $self->{isdtls});
522
523    $self->{partial}[$server] = $ret[2];
524    push @{$self->{record_list}}, @{$ret[0]};
525    push @{$self->{message_list}}, @{$ret[1]};
526
527    print "\n";
528
529    if (scalar(@{$ret[0]}) == 0 or length($ret[2]) != 0) {
530        return "";
531    }
532
533    #Finished parsing. Call user provided filter here
534    if (defined $self->filter) {
535        $self->filter->($self);
536    }
537
538    #Take a note on NewSessionTicket
539    foreach my $message (reverse @{$self->{message_list}}) {
540        if ($message->{mt} == TLSProxy::Message::MT_NEW_SESSION_TICKET) {
541            $self->{saw_session_ticket} = 1;
542            last;
543        }
544    }
545
546    #Reconstruct the packet
547    $packet = "";
548    foreach my $record (@{$self->record_list}) {
549        $packet .= $record->reconstruct_record($server);
550    }
551
552    print "Forwarded packet length = ".length($packet)."\n\n";
553
554    return $packet;
555}
556
557#Read accessors
558sub execute
559{
560    my $self = shift;
561    return $self->{execute};
562}
563sub cert
564{
565    my $self = shift;
566    return $self->{cert};
567}
568sub debug
569{
570    my $self = shift;
571    return $self->{debug};
572}
573sub flight
574{
575    my $self = shift;
576    return $self->{flight};
577}
578sub record_list
579{
580    my $self = shift;
581    return $self->{record_list};
582}
583sub success
584{
585    my $self = shift;
586    return $self->{success};
587}
588sub end
589{
590    my $self = shift;
591    return $self->{end};
592}
593sub supports_IPv6
594{
595    my $self = shift;
596    return $have_IPv6;
597}
598sub proxy_addr
599{
600    my $self = shift;
601    return $self->{proxy_addr};
602}
603sub proxy_port
604{
605    my $self = shift;
606    return $self->{proxy_port};
607}
608sub server_addr
609{
610    my $self = shift;
611    return $self->{server_addr};
612}
613sub server_port
614{
615    my $self = shift;
616    return $self->{server_port};
617}
618sub serverpid
619{
620    my $self = shift;
621    return $self->{serverpid};
622}
623sub clientpid
624{
625    my $self = shift;
626    return $self->{clientpid};
627}
628
629#Read/write accessors
630sub filter
631{
632    my $self = shift;
633    if (@_) {
634        $self->{filter} = shift;
635    }
636    return $self->{filter};
637}
638sub cipherc
639{
640    my $self = shift;
641    if (@_) {
642        $self->{cipherc} = shift;
643    }
644    return $self->{cipherc};
645}
646sub ciphersuitesc
647{
648    my $self = shift;
649    if (@_) {
650        $self->{ciphersuitesc} = shift;
651    }
652    return $self->{ciphersuitesc};
653}
654sub ciphers
655{
656    my $self = shift;
657    if (@_) {
658        $self->{ciphers} = shift;
659    }
660    return $self->{ciphers};
661}
662sub ciphersuitess
663{
664    my $self = shift;
665    if (@_) {
666        $self->{ciphersuitess} = shift;
667    }
668    return $self->{ciphersuitess};
669}
670sub serverflags
671{
672    my $self = shift;
673    if (@_) {
674        $self->{serverflags} = shift;
675    }
676    return $self->{serverflags};
677}
678sub clientflags
679{
680    my $self = shift;
681    if (@_) {
682        $self->{clientflags} = shift;
683    }
684    return $self->{clientflags};
685}
686sub serverconnects
687{
688    my $self = shift;
689    if (@_) {
690        $self->{serverconnects} = shift;
691    }
692    return $self->{serverconnects};
693}
694# This is a bit ugly because the caller is responsible for keeping the records
695# in sync with the updated message list; simply updating the message list isn't
696# sufficient to get the proxy to forward the new message.
697# But it does the trick for the one test (test_sslsessiontick) that needs it.
698sub message_list
699{
700    my $self = shift;
701    if (@_) {
702        $self->{message_list} = shift;
703    }
704    return $self->{message_list};
705}
706
707sub fill_known_data
708{
709    my $length = shift;
710    my $ret = "";
711    for (my $i = 0; $i < $length; $i++) {
712        $ret .= chr($i);
713    }
714    return $ret;
715}
716
717sub is_tls13
718{
719    my $class = shift;
720    if (@_) {
721        $is_tls13 = shift;
722    }
723    return $is_tls13;
724}
725
726sub reneg
727{
728    my $self = shift;
729    if (@_) {
730        $self->{reneg} = shift;
731    }
732    return $self->{reneg};
733}
734
735#Setting a sessionfile means that the client will not close until the given
736#file exists. This is useful in TLSv1.3 where otherwise s_client will close
737#immediately at the end of the handshake, but before the session has been
738#received from the server. A side effect of this is that s_client never sends
739#a close_notify, so instead we consider success to be when it sends application
740#data over the connection.
741sub sessionfile
742{
743    my $self = shift;
744    if (@_) {
745        $self->{sessionfile} = shift;
746        TLSProxy::Message->successondata(1);
747    }
748    return $self->{sessionfile};
749}
750
751sub ciphersuite
752{
753    my $class = shift;
754    if (@_) {
755        $ciphersuite = shift;
756    }
757    return $ciphersuite;
758}
759
760sub isdtls
761{
762    my $self = shift;
763    return $self->{isdtls}; #read-only
764}
765
7661;
767