mirror of https://github.com/sipwise/rtpengine.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
326 lines
7.2 KiB
326 lines
7.2 KiB
package NGCP::Rtpclient::DTLS;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use NGCP::Rtpclient::SRTP;
|
|
use File::Temp;
|
|
use Crypt::OpenSSL::RSA;
|
|
use IO::Socket::INET;
|
|
use IPC::Open3;
|
|
use IO::Multiplex;
|
|
use Time::HiRes qw(sleep time);
|
|
|
|
sub new {
|
|
my ($class, $mux, $local_sockets, $output_func, $tag, $cert) = @_;
|
|
|
|
my $self = {};
|
|
bless $self, $class;
|
|
|
|
$self->{_output_func} = $output_func;
|
|
$self->{_mux} = $mux;
|
|
$self->{_tag} = $tag;
|
|
$self->{_local_sockets} = $local_sockets;
|
|
|
|
if ($cert) {
|
|
$self->set_cert($cert);
|
|
}
|
|
else {
|
|
$self->new_cert();
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub new_cert {
|
|
my ($self) = @_;
|
|
|
|
my $rsa_key = Crypt::OpenSSL::RSA->generate_key(1024);
|
|
my $priv_key = $rsa_key->get_private_key_string();
|
|
my $key_file = File::Temp->new();
|
|
print $key_file $priv_key;
|
|
close($key_file);
|
|
|
|
my $cert_file = File::Temp->new();
|
|
system(qw(openssl req -key), $key_file->filename(), '-out', $cert_file->filename(),
|
|
qw(-new -x509 -days 30 -subj /CN=tester -batch));
|
|
my $cert;
|
|
read($cert_file, $cert, 10000);
|
|
close($cert_file);
|
|
|
|
my $cert_key_file = File::Temp->new();
|
|
print $cert_key_file $cert;
|
|
print $cert_key_file $priv_key;
|
|
close($cert_key_file);
|
|
|
|
$self->set_cert($cert_key_file);
|
|
return $cert_key_file;
|
|
}
|
|
|
|
sub get_cert {
|
|
my ($self) = @_;
|
|
return $self->{_cert_key_file};
|
|
}
|
|
|
|
sub set_cert {
|
|
my ($self, $file) = @_;
|
|
$self->{_cert_key_file} = $file;
|
|
}
|
|
|
|
# XXX unify these two
|
|
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
|
|
my ($self) = @_;
|
|
|
|
$self->{_connected} and return;
|
|
|
|
$self->_kill_openssl_child();
|
|
|
|
my $near = $self->{_near};
|
|
|
|
$near or ($near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
|
|
Proto => 'udp'));
|
|
|
|
my $near_port = $near->sockport();
|
|
|
|
my ($openssl_in, $openssl_out);
|
|
$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
|
|
qw(openssl s_client -connect),
|
|
"localhost:$near_port",
|
|
'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1 -use_srtp
|
|
SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
|
|
-keymatexportlen 60));
|
|
$self->{_openssl_out} = $openssl_out;
|
|
$self->{_openssl_in} = $openssl_in;
|
|
$self->{_openssl_buf} = '';
|
|
|
|
$self->{_mux}->add($near);
|
|
$self->{_mux}->add($openssl_out);
|
|
}
|
|
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
|
|
my ($self) = @_;
|
|
|
|
$self->{_connected} and return;
|
|
|
|
$self->_kill_openssl_child();
|
|
|
|
my ($near_port, $near_peer);
|
|
my $near = $self->{_near};
|
|
if ($near) {
|
|
$near_port = $near->peerport();
|
|
$near_peer = $near->peeraddr();
|
|
}
|
|
else {
|
|
my $tmp = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost', Proto => 'udp');
|
|
$near_port = $tmp->sockport();
|
|
undef($tmp);
|
|
|
|
$near = $self->{_near} = IO::Socket::INET->new(Type => SOCK_DGRAM, LocalAddr => 'localhost',
|
|
Proto => 'udp');
|
|
$near_peer = pack_sockaddr_in($near_port, inet_aton("localhost"));
|
|
# $near gets connected below
|
|
}
|
|
|
|
my ($openssl_in, $openssl_out);
|
|
$self->{_openssl_pid} = open3($openssl_in, $openssl_out, undef,
|
|
qw(openssl s_server -accept),
|
|
$near_port,
|
|
'-cert', $self->{_cert_key_file}->filename(), qw(-dtls1 -use_srtp
|
|
SRTP_AES128_CM_SHA1_80:SRTP_AES128_CM_SHA1_32 -keymatexport EXTRACTOR-dtls_srtp
|
|
-keymatexportlen 60));
|
|
# XXX dtls 1.2 ?
|
|
|
|
sleep(0.2); # given openssl a short while to start up
|
|
|
|
$self->_near_peer($near_peer);
|
|
|
|
$self->{_openssl_out} = $openssl_out;
|
|
$self->{_openssl_in} = $openssl_in;
|
|
$self->{_openssl_buf} = '';
|
|
|
|
$self->{_mux}->add($near);
|
|
$self->{_mux}->add($openssl_out);
|
|
}
|
|
|
|
sub _kill_openssl_child {
|
|
my ($self) = @_;
|
|
|
|
if ($self->{_openssl_pid}) {
|
|
kill(9, $self->{_openssl_pid});
|
|
waitpid($self->{_openssl_pid}, 0);
|
|
}
|
|
delete($self->{_openssl_pid});
|
|
delete($self->{_openssl_in});
|
|
delete($self->{_openssl_out});
|
|
}
|
|
|
|
sub DESTROY {
|
|
my ($self) = @_;
|
|
|
|
$self->_kill_openssl_child();
|
|
}
|
|
|
|
sub _openssl_input {
|
|
my ($self, $fh, $s_r, $peer) = @_;
|
|
|
|
if ($self->{_openssl_done}) {
|
|
$$s_r = '';
|
|
return;
|
|
}
|
|
|
|
$self->{_openssl_buf} .= $$s_r;
|
|
$$s_r = '';
|
|
|
|
if ($self->{_openssl_buf} =~ /Server certificate\n(-----BEGIN CERTIFICATE-----\n.*?\n-----END CERTIFICATE-----\n).*SRTP Extension negotiated, profile=(\S+).*Keying material: ([0-9a-fA-F]{120})/s) {
|
|
$self->{_peer_cert} = $1;
|
|
$self->{_profile} = $2;
|
|
$self->{_keys} = pack('H*', $3);
|
|
$self->{_connected} = 1;
|
|
$self->{_openssl_done} = 1;
|
|
}
|
|
if ($self->{_openssl_buf} =~ /\nDONE\n/s) {
|
|
$self->{_openssl_done} = 1;
|
|
}
|
|
}
|
|
|
|
sub _near_peer {
|
|
my ($self, $peer) = @_;
|
|
|
|
$self->{_near_peer} and return;
|
|
|
|
$self->{_near_peer} = $peer;
|
|
CORE::connect($self->{_near}, $self->{_near_peer});
|
|
}
|
|
|
|
sub _near_input {
|
|
my ($self, $fh, $s_r, $peer) = @_;
|
|
|
|
my $func = $self->{_output_func};
|
|
if (ref($func) eq 'CODE') {
|
|
$func->($self->{_tag}, $$s_r);
|
|
}
|
|
else {
|
|
# object
|
|
$func->dtls_send($self->{_tag}, $$s_r);
|
|
}
|
|
|
|
$self->_near_peer($peer);
|
|
|
|
$$s_r = '';
|
|
}
|
|
|
|
sub input {
|
|
my ($self, $fh, $s_r, $peer) = @_;
|
|
|
|
$$s_r eq '' and return;
|
|
|
|
if ($fh == $self->{_openssl_out}) { # openssl's stdout
|
|
return $self->_openssl_input($fh, $s_r, $peer);
|
|
}
|
|
elsif ($fh == $self->{_near}) { # UDP input from openssl - forward to peer
|
|
return $self->_near_input($fh, $s_r, $peer);
|
|
}
|
|
|
|
# UDP input from peer - demux and forward to openssl
|
|
is_dtls($$s_r) or return;
|
|
$self->{_near_peer} or return; # nowhere to forward it to
|
|
grep {$fh == $_} @{$self->{_local_sockets}} or return; # not one of ours
|
|
|
|
send($self->{_near}, $$s_r, 0);
|
|
$$s_r = '';
|
|
}
|
|
|
|
sub peer_cert {
|
|
my ($self) = @_;
|
|
$self->{_peer_cert_file} and return $self->{_peer_cert_file};
|
|
$self->{_peer_cert} or return;
|
|
|
|
my $cert_file = File::Temp->new();
|
|
print $cert_file $self->{_peer_cert};
|
|
close($cert_file);
|
|
|
|
return ($self->{_peer_cert_file} = $cert_file);
|
|
}
|
|
|
|
sub cert_fingerprint {
|
|
my ($cert_file) = @_;
|
|
my $fd;
|
|
open($fd, '-|', qw(openssl x509 -in), $cert_file->filename(), qw(-fingerprint -noout));
|
|
my $fp = <$fd>;
|
|
close($fd);
|
|
$fp =~ /SHA1 Fingerprint=([0-9a-f:]+)/i or return;
|
|
return $1;
|
|
}
|
|
|
|
sub fingerprint {
|
|
my ($self) = @_;
|
|
return cert_fingerprint($self->{_cert_key_file});
|
|
}
|
|
|
|
sub get_keys {
|
|
my ($self) = @_;
|
|
|
|
$self->{_keys} =~ /^(.{16})(.{16})(.{14})(.{14})$/s or return;
|
|
return ($self->{_profile}, $1, $2, $3, $4);
|
|
}
|
|
|
|
sub is_dtls {
|
|
my ($s) = @_;
|
|
length($s) < 1 and return 0;
|
|
my $c = ord(substr($s, 0, 1));
|
|
$c < 20 and return 0;
|
|
$c > 63 and return 0;
|
|
return 1;
|
|
}
|
|
|
|
sub encode {
|
|
my ($self) = @_;
|
|
my @ret;
|
|
push(@ret, 'a=setup:actpass');
|
|
push(@ret, 'a=fingerprint:sha-1 ' . $self->fingerprint());
|
|
return @ret;
|
|
}
|
|
|
|
package NGCP::Rtpclient::DTLS::Group;
|
|
|
|
sub new {
|
|
my ($class, $mux, $output_func, $socket_components, $cert) = @_;
|
|
|
|
my $self = [];
|
|
bless $self, $class;
|
|
|
|
my $max_component = $#{$socket_components};
|
|
|
|
for my $idx (0 .. $max_component) {
|
|
my $local_sockets = $socket_components->[$idx];
|
|
my $cl = NGCP::Rtpclient::DTLS->new($mux, $local_sockets, $output_func, $idx, $cert);
|
|
push(@$self, $cl);
|
|
$cert = $cl->get_cert();
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub encode {
|
|
my ($self, @rest) = @_;
|
|
return $self->[0]->encode(@rest);
|
|
}
|
|
sub connect { ## no critic: Subroutines::ProhibitBuiltinHomonyms
|
|
my ($self, @rest) = @_;
|
|
for my $cl (@$self) {
|
|
$cl->accept(@rest);
|
|
}
|
|
}
|
|
sub accept { ## no critic: Subroutines::ProhibitBuiltinHomonyms
|
|
my ($self, @rest) = @_;
|
|
for my $cl (@$self) {
|
|
$cl->accept(@rest);
|
|
}
|
|
}
|
|
sub input {
|
|
my ($self, @rest) = @_;
|
|
for my $cl (@$self) {
|
|
$cl->input(@rest);
|
|
}
|
|
}
|
|
|
|
1;
|