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