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