|
|
@ -0,0 +1,803 @@ |
|
|
|
|
|
package ICE; |
|
|
|
|
|
|
|
|
|
|
|
use strict; |
|
|
|
|
|
use warnings; |
|
|
|
|
|
use Socket; |
|
|
|
|
|
use Socket6; |
|
|
|
|
|
use IO::Socket; |
|
|
|
|
|
use IO::Multiplex; |
|
|
|
|
|
use Math::BigInt; |
|
|
|
|
|
use Digest::HMAC_SHA1 qw(hmac_sha1); |
|
|
|
|
|
use Digest::CRC qw(crc32); |
|
|
|
|
|
use Time::HiRes qw(time); |
|
|
|
|
|
|
|
|
|
|
|
my @ice_chars = ('A' .. 'Z', 'a' .. 'z', '0' .. '9'); |
|
|
|
|
|
my %type_preferences = ( |
|
|
|
|
|
host => 126, |
|
|
|
|
|
srflx => 100, |
|
|
|
|
|
prflx => 110, |
|
|
|
|
|
relay => 0, |
|
|
|
|
|
); |
|
|
|
|
|
my %protocols = ( 17 => 'UDP' ); |
|
|
|
|
|
|
|
|
|
|
|
sub random_string { |
|
|
|
|
|
my ($len) = @_; |
|
|
|
|
|
return join('', (map {$ice_chars[rand(@ice_chars)]} (1 .. $len))); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub new { |
|
|
|
|
|
my ($class, $components, $controlling) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my $self = {}; |
|
|
|
|
|
bless $self, $class; |
|
|
|
|
|
|
|
|
|
|
|
$self->{my_ufrag} = random_string(8); |
|
|
|
|
|
$self->{my_pwd} = random_string(26); |
|
|
|
|
|
$self->{controlling} = $controlling; |
|
|
|
|
|
$self->{components} = $components; |
|
|
|
|
|
$self->{tie_breaker} = i64from32(rand(2**32), rand(2**32)); |
|
|
|
|
|
|
|
|
|
|
|
$self->{candidates} = {}; # foundation -> candidate |
|
|
|
|
|
$self->{remote_candidates} = {}; # foundation -> candidate |
|
|
|
|
|
$self->{candidate_pairs} = {}; # foundation pairs -> pair |
|
|
|
|
|
$self->{remote_peers} = {}; # peer_hash_key -> component |
|
|
|
|
|
$self->{changed_foundations} = {}; # old -> new |
|
|
|
|
|
|
|
|
|
|
|
$self->{triggered_checks} = []; |
|
|
|
|
|
$self->{last_timer} = 0; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("created, controll" . ($controlling ? "ing" : "ed") |
|
|
|
|
|
. ", tie breaker " . $self->{tie_breaker}->bstr() . "\n"); |
|
|
|
|
|
|
|
|
|
|
|
return $self; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub i64from32 { |
|
|
|
|
|
my ($hi, $lo) = @_; |
|
|
|
|
|
my $i = Math::BigInt->new(int($hi)); |
|
|
|
|
|
$i->blsft(32); |
|
|
|
|
|
$i->badd(int($lo)); |
|
|
|
|
|
return $i; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub calc_priority { |
|
|
|
|
|
my ($type, $local_pref, $component) = @_; |
|
|
|
|
|
defined($type_preferences{$type}) or die; |
|
|
|
|
|
return (2 ** 24) * $type_preferences{$type} + (2 ** 8) * $local_pref + (256 - $component); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub add_candidate { |
|
|
|
|
|
my ($self, $local_pref, $type, @components) = @_; |
|
|
|
|
|
# highest local pref = 65535, lowest = 0 |
|
|
|
|
|
|
|
|
|
|
|
@components == $self->{components} or die; |
|
|
|
|
|
defined($type_preferences{$type}) or die; |
|
|
|
|
|
|
|
|
|
|
|
my $foundation = random_string(16); |
|
|
|
|
|
my $cands = $self->{candidates}; |
|
|
|
|
|
$cands->{$foundation} and die; |
|
|
|
|
|
|
|
|
|
|
|
my $comps = []; |
|
|
|
|
|
my $comp_id = 1; |
|
|
|
|
|
for my $c (@components) { |
|
|
|
|
|
my $comp = { socket => $c, component => $comp_id, |
|
|
|
|
|
priority => calc_priority($type, $local_pref, $comp_id), |
|
|
|
|
|
foundation => $foundation, |
|
|
|
|
|
protocol => 'UDP', af => $c->sockdomain(), |
|
|
|
|
|
address => $c->sockhost(), port => $c->sockport() }; |
|
|
|
|
|
push(@$comps, $comp); |
|
|
|
|
|
$self->debug("$foundation/$comp_id is $comp->{address}/$comp->{port}\n"); |
|
|
|
|
|
|
|
|
|
|
|
$comp_id++; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$cands->{$foundation} = { foundation => $foundation, preference => $local_pref, |
|
|
|
|
|
base_priority => calc_priority($type, $local_pref, 0), |
|
|
|
|
|
type => $type, components => $comps, protocol => 'UDP', |
|
|
|
|
|
af => $comps->[0]->{af}, address => $comps->[0]->{address} }; |
|
|
|
|
|
|
|
|
|
|
|
$self->pair_candidates(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub encode { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my @ret; |
|
|
|
|
|
|
|
|
|
|
|
push(@ret, "a=ice-ufrag:$self->{my_ufrag}"); |
|
|
|
|
|
push(@ret, "a=ice-pwd:$self->{my_pwd}"); |
|
|
|
|
|
|
|
|
|
|
|
for my $cand (values(%{$self->{candidates}})) { |
|
|
|
|
|
for my $comp (@{$cand->{components}}) { |
|
|
|
|
|
my $prot = $comp->{socket}->protocol(); |
|
|
|
|
|
my $sa = $comp->{socket}->sockhost(); |
|
|
|
|
|
my $sp = $comp->{socket}->sockport(); |
|
|
|
|
|
push(@ret, "a=candidate:$cand->{foundation} $comp->{component} $protocols{$prot} $comp->{priority} $sa $sp typ $cand->{type}"); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return @ret; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub remote_foundation_change { |
|
|
|
|
|
my ($self, $old, $new, $type) = @_; |
|
|
|
|
|
|
|
|
|
|
|
if ($self->{changed_foundations}->{$old}) { |
|
|
|
|
|
$self->{changed_foundations}->{$old} eq $new or die; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
$self->debug("changing remote candidate foundation from $old to $new\n"); |
|
|
|
|
|
my $old_cand = $self->{remote_candidates}->{$old} or die; |
|
|
|
|
|
$old_cand->{type} = $type; |
|
|
|
|
|
$old_cand->{foundation} = $new; |
|
|
|
|
|
|
|
|
|
|
|
for my $comp (@{$old_cand->{components}}) { |
|
|
|
|
|
$comp->{foundation} = $new; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for my $foundation_pair (keys(%{$self->{candidate_pairs}})) { |
|
|
|
|
|
my $pair = $self->{candidate_pairs}->{$foundation_pair}; |
|
|
|
|
|
$pair->{remote} == $old_cand or next; |
|
|
|
|
|
|
|
|
|
|
|
my $new_foundation = $pair->{local}->{foundation} . $new; |
|
|
|
|
|
delete($self->{candidate_pairs}->{$foundation_pair}); |
|
|
|
|
|
$self->{candidate_pairs}->{$new_foundation} = $pair; |
|
|
|
|
|
|
|
|
|
|
|
for my $comp (@{$pair->{components}}) { |
|
|
|
|
|
$comp->{foundation} = $new_foundation; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$self->{remote_candidates}->{$new} = $old_cand; |
|
|
|
|
|
delete($self->{remote_candidates}->{$old}); |
|
|
|
|
|
$self->{changed_foundations}->{$old} = $new; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub new_remote_candidate { |
|
|
|
|
|
my ($self, $cand_str) = @_; |
|
|
|
|
|
$self->_new_remote_candidates_start(); |
|
|
|
|
|
my $ret = $self->_new_remote_candidate($cand_str); |
|
|
|
|
|
$self->_got_new_candidates(); |
|
|
|
|
|
return $ret; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub _new_remote_candidates_start { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
$self->{new_candidates} = {}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub _new_remote_candidate { |
|
|
|
|
|
my ($self, $c) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("adding remote candidate $c\n"); |
|
|
|
|
|
my ($foundation, $component, $protocol, $priority, $address, $port, $type) |
|
|
|
|
|
= $c =~ /^(\w+) (\d) (\w+) (\d+) ([0-9a-fA-F:.]+) (\d+) typ (\w+)/ or die $c; |
|
|
|
|
|
|
|
|
|
|
|
$protocol = uc($protocol); |
|
|
|
|
|
my $phk = "$protocol/$address/$port"; |
|
|
|
|
|
|
|
|
|
|
|
if (my $old = $self->{remote_peers}->{$phk}) { |
|
|
|
|
|
# must be a previously learned prflx candidate |
|
|
|
|
|
$old = $old->{candidate}; |
|
|
|
|
|
$old->{type} eq 'prflx' or die; |
|
|
|
|
|
# replace the learned prflx candidate with the new one |
|
|
|
|
|
$self->remote_foundation_change($old->{foundation}, $foundation, $type); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
my $f = ($self->{new_candidates}->{$foundation} // ( |
|
|
|
|
|
$self->{new_candidates}->{$foundation} = { |
|
|
|
|
|
foundation => $foundation, |
|
|
|
|
|
type => $type, |
|
|
|
|
|
protocol => $protocol, |
|
|
|
|
|
components => [], |
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
$f->{type} eq $type or die; |
|
|
|
|
|
$f->{protocol} eq $protocol or die; |
|
|
|
|
|
|
|
|
|
|
|
$f->{components}->[$component - 1] and die; |
|
|
|
|
|
my $comp = $f->{components}->[$component - 1] = { |
|
|
|
|
|
candidate => $f, |
|
|
|
|
|
foundation => $foundation, |
|
|
|
|
|
component => $component, |
|
|
|
|
|
priority => $priority, |
|
|
|
|
|
address => $address, |
|
|
|
|
|
port => $port, |
|
|
|
|
|
peer_hash_key => $phk, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
if ($address =~ /^\d+\.\d+\.\d+\.\d+$/) { |
|
|
|
|
|
$f->{af} = $comp->{af} = &AF_INET; |
|
|
|
|
|
$comp->{packed_peer} = pack_sockaddr_in($port, inet_pton(&AF_INET, $address)); |
|
|
|
|
|
} |
|
|
|
|
|
elsif ($address =~ /^[0-9a-fA-F:]+$/) { |
|
|
|
|
|
$f->{af} = $comp->{af} = &AF_INET6; |
|
|
|
|
|
$comp->{packed_peer} = pack_sockaddr_in6($port, inet_pton(&AF_INET6, $address)); |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
die; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$self->{remote_peers}->{$phk} = $comp; |
|
|
|
|
|
|
|
|
|
|
|
return $comp; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub _got_new_candidates { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
|
|
|
|
|
|
# validate received info and eliminate duplicates |
|
|
|
|
|
my $r_cand = $self->{remote_candidates}; |
|
|
|
|
|
my $r_peers = $self->{remote_peers}; |
|
|
|
|
|
for my $c (values(%{$self->{new_candidates}})) { |
|
|
|
|
|
# @{$c->{components}} == $self->{components} or die; |
|
|
|
|
|
|
|
|
|
|
|
if (my $exist = $r_cand->{$c->{foundation}}) { |
|
|
|
|
|
# duplicate. OK if this is a learned prflx |
|
|
|
|
|
if ($exist->{type} eq 'prflx' && $c->{type} eq 'prflx') { |
|
|
|
|
|
# merge components |
|
|
|
|
|
for my $idx (0 .. $#{$c->{components}}) { |
|
|
|
|
|
defined($c->{components}->[$idx]) or next; |
|
|
|
|
|
defined($exist->{components}->[$idx]) and die; |
|
|
|
|
|
$exist->{components}->[$idx] = $c->{components}->[$idx]; |
|
|
|
|
|
} |
|
|
|
|
|
next; |
|
|
|
|
|
} |
|
|
|
|
|
warn; |
|
|
|
|
|
next; |
|
|
|
|
|
} |
|
|
|
|
|
$r_cand->{$c->{foundation}} = $c; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
delete($self->{new_candidates}); |
|
|
|
|
|
$self->pair_candidates(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
sub decode { |
|
|
|
|
|
my ($self, $h) = @_; |
|
|
|
|
|
# $h is output of SDP::Media->decode_ice() |
|
|
|
|
|
|
|
|
|
|
|
$self->{other_ufrag} = $h->{ufrag} or die; |
|
|
|
|
|
$self->{other_pwd} = $h->{pwd} or die; |
|
|
|
|
|
|
|
|
|
|
|
my $cands = $h->{candidates} or die; |
|
|
|
|
|
$self->_new_remote_candidates_start(); |
|
|
|
|
|
for my $c (@$cands) { |
|
|
|
|
|
$self->_new_remote_candidate($c); |
|
|
|
|
|
} |
|
|
|
|
|
$self->_got_new_candidates(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub pair_candidates { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my $pairs = $self->{candidate_pairs}; |
|
|
|
|
|
|
|
|
|
|
|
for my $rem (values(%{$self->{remote_candidates}})) { |
|
|
|
|
|
for my $loc (values(%{$self->{candidates}})) { |
|
|
|
|
|
$loc->{protocol} eq $rem->{protocol} or next; |
|
|
|
|
|
$loc->{af} == $rem->{af} or next; |
|
|
|
|
|
|
|
|
|
|
|
@{$loc->{components}} == $self->{components} or die; |
|
|
|
|
|
|
|
|
|
|
|
my $foundation = $loc->{foundation} . $rem->{foundation}; |
|
|
|
|
|
my $pair = $pairs->{$foundation} || ($pairs->{$foundation} = |
|
|
|
|
|
{ foundation => $foundation, local => $loc, remote => $rem, |
|
|
|
|
|
components => []} |
|
|
|
|
|
); |
|
|
|
|
|
my $comps = $pair->{components}; |
|
|
|
|
|
|
|
|
|
|
|
for my $idx (0 .. ($self->{components} - 1)) { |
|
|
|
|
|
defined($loc->{components}->[$idx]) or next; |
|
|
|
|
|
defined($rem->{components}->[$idx]) or next; |
|
|
|
|
|
|
|
|
|
|
|
my $c = $comps->[$idx] || ($comps->[$idx] = |
|
|
|
|
|
{ foundation => $foundation, |
|
|
|
|
|
local => $loc->{components}->[$idx], |
|
|
|
|
|
remote => $rem->{components}->[$idx] }); |
|
|
|
|
|
$c->{state} = $c->{state} || ($idx == 0 ? 'waiting' : 'frozen'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub pair_priority { |
|
|
|
|
|
my ($self, $pair) = @_; |
|
|
|
|
|
# could be a candidate pair or a component pair. only components have priorities |
|
|
|
|
|
my $gk = $self->{controlling} ? 'local' : 'remote'; |
|
|
|
|
|
my $dk = $self->{controlling} ? 'remote' : 'local'; |
|
|
|
|
|
my $gc = $pair->{$gk}; |
|
|
|
|
|
my $dc = $pair->{$dk}; |
|
|
|
|
|
if (exists($gc->{components})) { |
|
|
|
|
|
$gc = $gc->{components}->[0]; |
|
|
|
|
|
$dc = $dc->{components}->[0]; |
|
|
|
|
|
} |
|
|
|
|
|
my $g = $gc->{priority}; |
|
|
|
|
|
my $d = $dc->{priority}; |
|
|
|
|
|
return (($g < $d ? $g : $d) << 32) + (($g > $d ? $g : $d) * 2) + ($g > $d ? 1 : 0); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub get_pair { |
|
|
|
|
|
my ($self, $local, $remote, $component) = @_; |
|
|
|
|
|
my $found = "$local$remote"; |
|
|
|
|
|
my $pair = $self->{candidate_pairs}->{$found} or return; |
|
|
|
|
|
$component or return $pair; |
|
|
|
|
|
return $pair->{components}->[$component - 1]; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub is_ice { |
|
|
|
|
|
my ($s) = @_; |
|
|
|
|
|
|
|
|
|
|
|
length($s) < 20 and return 0; |
|
|
|
|
|
my $c = ord(substr($s, 0, 1)); |
|
|
|
|
|
($c & 0xb0) != 0 and return 0; |
|
|
|
|
|
$c = ord(substr($s, 3, 1)); |
|
|
|
|
|
($c & 0x03) != 0 and return 0; |
|
|
|
|
|
$c = substr($s, 4, 4); |
|
|
|
|
|
$c ne "\x21\x12\xA4\x42" and return 0; |
|
|
|
|
|
return 1; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub input { |
|
|
|
|
|
my ($self, $fh, $s_r, $peer) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$$s_r eq '' and return; |
|
|
|
|
|
is_ice($$s_r) or return; |
|
|
|
|
|
|
|
|
|
|
|
for my $cands (values(%{$self->{candidates}})) { |
|
|
|
|
|
for my $comp (@{$cands->{components}}) { |
|
|
|
|
|
$fh == $comp->{socket} or next; |
|
|
|
|
|
$self->do_input($comp, $$s_r, $peer); |
|
|
|
|
|
$$s_r = ''; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
my %attr_handlers = ( |
|
|
|
|
|
0x0006 => \&stun_handler_USERNAME, |
|
|
|
|
|
0x0008 => \&stun_handler_MESSAGE_INTEGRITY, |
|
|
|
|
|
0x0009 => \&stun_handler_ERROR_CODE, |
|
|
|
|
|
0x000a => \&stun_handler_UNKNOWN_ATTRIBUTES, |
|
|
|
|
|
0x0020 => \&stun_handler_XOR_MAPPED_ADDRESS, |
|
|
|
|
|
0x0024 => \&stun_handler_PRIORITY, |
|
|
|
|
|
0x0025 => \&stun_handler_USE_CANDIDATE, |
|
|
|
|
|
0x8022 => \&stun_handler_SOFTWARE, |
|
|
|
|
|
0x8028 => \&stun_handler_FINGERPRINT, |
|
|
|
|
|
0x8029 => \&stun_handler_ICE_CONTROLLED, |
|
|
|
|
|
0x802a => \&stun_handler_ICE_CONTROLLING, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
my %type_handlers = ( |
|
|
|
|
|
1 => \&stun_handler_binding_request, |
|
|
|
|
|
17 => \&stun_handler_binding_indication, |
|
|
|
|
|
257 => \&stun_handler_binding_success, |
|
|
|
|
|
273 => \&stun_handler_binding_error, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
sub do_input { |
|
|
|
|
|
my ($self, $comp, $s, $peer) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my $hdr = substr($s, 0, 20, ''); |
|
|
|
|
|
my ($mtype, $mlen, $cookie, $tid) = unpack('nnNa12', $hdr); |
|
|
|
|
|
$cookie == 0x2112A442 or return; |
|
|
|
|
|
|
|
|
|
|
|
my (@stack, %hash); |
|
|
|
|
|
|
|
|
|
|
|
while (my ($type, $len) = unpack('nn', $s)) { |
|
|
|
|
|
my $padding = 4 - ($len % 4); |
|
|
|
|
|
$padding == 4 and $padding = 0; |
|
|
|
|
|
|
|
|
|
|
|
my $raw = substr($s, 0, 4 + $len + $padding); |
|
|
|
|
|
|
|
|
|
|
|
substr($s, 0, 4) = ''; |
|
|
|
|
|
my $data = substr($s, 0, $len, ''); |
|
|
|
|
|
substr($s, 0, $padding) = ''; |
|
|
|
|
|
|
|
|
|
|
|
my $handler = $attr_handlers{$type}; |
|
|
|
|
|
if (!$handler) { |
|
|
|
|
|
warn("unknown STUN attribute $type data $data"); |
|
|
|
|
|
next; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
my $parsed = $handler->($data, $tid) or die; |
|
|
|
|
|
$parsed->{raw} = $raw; |
|
|
|
|
|
|
|
|
|
|
|
push(@stack, $parsed); |
|
|
|
|
|
$hash{$parsed->{name}} = $parsed; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$stack[$#stack]->{name} eq 'fingerprint' or die; |
|
|
|
|
|
$stack[$#stack - 1]->{name} eq 'integrity' or die; |
|
|
|
|
|
|
|
|
|
|
|
my $pwd_check = $mtype == 1 ? $self->{my_pwd} : $self->{other_pwd}; |
|
|
|
|
|
# XXX unify these with sub integrity/fingerprint ? |
|
|
|
|
|
my $int_check = join('', (map {$_->{raw}} @stack[0 .. ($#stack - 2)])); |
|
|
|
|
|
$int_check = pack('nnNa12', $mtype, length($int_check) + 24, $cookie, $tid) . $int_check; |
|
|
|
|
|
my $digest = hmac_sha1($int_check, $pwd_check); |
|
|
|
|
|
$digest eq $hash{integrity}->{digest} or die; |
|
|
|
|
|
|
|
|
|
|
|
my $fp_check = join('', (map {$_->{raw}} @stack[0 .. ($#stack - 1)])); |
|
|
|
|
|
$fp_check = pack('nnNa12', $mtype, length($fp_check) + 8, $cookie, $tid) . $fp_check; |
|
|
|
|
|
my $crc = crc32($fp_check); |
|
|
|
|
|
($crc ^ 0x5354554e) == $hash{fingerprint}->{crc} or die; |
|
|
|
|
|
|
|
|
|
|
|
# decode peer address |
|
|
|
|
|
my $domain = $comp->{af}; |
|
|
|
|
|
my (@peer, $address); |
|
|
|
|
|
if ($domain == &AF_INET) { |
|
|
|
|
|
@peer = unpack_sockaddr_in($peer); |
|
|
|
|
|
} |
|
|
|
|
|
elsif ($domain == &AF_INET6) { |
|
|
|
|
|
@peer = unpack_sockaddr_in6($peer); |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
die; |
|
|
|
|
|
} |
|
|
|
|
|
$address = inet_ntop($domain, $peer[1]); |
|
|
|
|
|
|
|
|
|
|
|
# process it |
|
|
|
|
|
my $handler = $type_handlers{$mtype} or die; |
|
|
|
|
|
my $response = $handler->($self, $comp, \@stack, \%hash, $tid, $peer, $peer[1], $address, $peer[0]); |
|
|
|
|
|
|
|
|
|
|
|
if ($response) { |
|
|
|
|
|
# construct and send response packet |
|
|
|
|
|
$self->integrity($response->{attrs}, $response->{mtype}, $tid, $self->{my_pwd}); |
|
|
|
|
|
$self->fingerprint($response->{attrs}, $response->{mtype}, $tid); |
|
|
|
|
|
|
|
|
|
|
|
# XXX unify |
|
|
|
|
|
my $packet = join('', @{$response->{attrs}}); |
|
|
|
|
|
$packet = pack('nnNa12', $response->{mtype}, length($packet), 0x2112A442, $tid) . $packet; |
|
|
|
|
|
$comp->{socket}->send($packet, 0, $peer); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_reply { |
|
|
|
|
|
my ($self, $attrs, $mtype) = @_; |
|
|
|
|
|
|
|
|
|
|
|
unshift(@$attrs, attr(0x8022, 'perl:ICE.pm')); |
|
|
|
|
|
|
|
|
|
|
|
my $response = { mtype => $mtype, attrs => $attrs }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_success { |
|
|
|
|
|
my ($self, $attrs) = @_; |
|
|
|
|
|
return $self->stun_reply($attrs, 257); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_error { |
|
|
|
|
|
my ($self, $code, $msg) = @_; |
|
|
|
|
|
return $self->stun_reply([ attr(0x0009, pack('Na*', ((($code / 100) << 8) | ($code % 100)), $msg)) ], 273); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub debug { |
|
|
|
|
|
my ($self, @rest) = @_; |
|
|
|
|
|
print("ICE agent", ' ', $self->{my_ufrag}, ' - ', @rest); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub dummy_foundation { |
|
|
|
|
|
my ($protocol, $address) = @_; |
|
|
|
|
|
return $protocol . unpack('H*', $address); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_handler_binding_request { |
|
|
|
|
|
my ($self, $comp, $stack, $hash, $tid, $packed_peer, $packed_host, $address, $port) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$hash->{username}->{my_ufrag} eq $self->{my_ufrag} or die; |
|
|
|
|
|
|
|
|
|
|
|
# check role |
|
|
|
|
|
if ($self->{controlling} && $hash->{controlling}) { |
|
|
|
|
|
if ($self->{tie_breaker}->bcmp($hash->{controlling}->{tie_breaker}) >= 0) { |
|
|
|
|
|
$self->debug("returning 487 role conflict\n"); |
|
|
|
|
|
return $self->stun_error(487, "Role conflict"); |
|
|
|
|
|
} |
|
|
|
|
|
$self->debug("role conflict, switching to controlled\n"); |
|
|
|
|
|
$self->{controlling} = 0; |
|
|
|
|
|
} |
|
|
|
|
|
elsif (!$self->{controlling} && $hash->{controlled}) { |
|
|
|
|
|
if ($self->{tie_breaker}->bcmp($hash->{controlled}->{tie_breaker}) < 0) { |
|
|
|
|
|
$self->debug("returning 487 role conflict\n"); |
|
|
|
|
|
return $self->stun_error(487, "Role conflict"); |
|
|
|
|
|
} |
|
|
|
|
|
$self->debug("role conflict, switching to controlling\n"); |
|
|
|
|
|
$self->{controlling} = 1; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("binding request from $address/$port\n"); |
|
|
|
|
|
|
|
|
|
|
|
# check if peer is known - learn prflx candidates |
|
|
|
|
|
my $cand = $self->{remote_peers}->{"UDP/$address/$port"}; |
|
|
|
|
|
if (!$cand) { |
|
|
|
|
|
$cand = $self->new_remote_candidate(dummy_foundation('UDP', $packed_host) |
|
|
|
|
|
. " $comp->{component} UDP " |
|
|
|
|
|
. "$hash->{priority}->{priority} $address $port typ prflx"); |
|
|
|
|
|
# this also pairs up the new candidate, which goes against 7.2.1.3 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
# get candidate pair and trigger check |
|
|
|
|
|
my $pair = $self->get_pair($comp->{foundation}, $cand->{foundation}, $comp->{component}); |
|
|
|
|
|
$pair or die; |
|
|
|
|
|
$self->trigger_check($pair); |
|
|
|
|
|
|
|
|
|
|
|
# set and check nominations |
|
|
|
|
|
if ($hash->{use}) { |
|
|
|
|
|
$pair->{nominated} = 1; |
|
|
|
|
|
$self->debug("$pair->{foundation} - got nominated\n"); |
|
|
|
|
|
$self->{controlling} or $self->check_nominations(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
# construct response |
|
|
|
|
|
my $attrs = []; |
|
|
|
|
|
|
|
|
|
|
|
if ($comp->{af} == &AF_INET) { |
|
|
|
|
|
push(@$attrs, attr(0x0020, pack('nna4', 1, $port ^ 0x2112, $packed_host ^ "\x21\x12\xa4\x42"))); |
|
|
|
|
|
} |
|
|
|
|
|
elsif ($comp->{af} == &AF_INET6) { |
|
|
|
|
|
push(@$attrs, attr(0x0020, pack('nna16', 2, $port ^ 0x2112, |
|
|
|
|
|
$packed_host ^ ("\x21\x12\xa4\x42" . $tid)))); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return $self->stun_success($attrs); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub check_nominations { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my @nominated; |
|
|
|
|
|
|
|
|
|
|
|
for my $pair (values(%{$self->{candidate_pairs}})) { |
|
|
|
|
|
my @comps = @{$pair->{components}}; |
|
|
|
|
|
my @nominated_comps = grep {$_->{nominated}} @comps; |
|
|
|
|
|
@comps < $self->{components} and next; |
|
|
|
|
|
$self->debug("got fully nominated pair $pair->{foundation}\n"); |
|
|
|
|
|
push(@nominated, $pair); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!@nominated) { |
|
|
|
|
|
$self->debug("no fully nominated pairs yet\n"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@nominated = $self->sort_pairs(\@nominated); |
|
|
|
|
|
my $pair = $nominated[0]; |
|
|
|
|
|
$self->debug("highest priority nominated pair is $pair->{foundation}\n"); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_handler_binding_success { |
|
|
|
|
|
my ($self, $comp, $stack, $hash, $tid, $packed_peer, $packed_host, $address, $port) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("binding success from $address/$port\n"); |
|
|
|
|
|
|
|
|
|
|
|
# check xor address |
|
|
|
|
|
$comp->{address} eq $hash->{address}->{address} or die; |
|
|
|
|
|
$comp->{port} == $hash->{address}->{port} or die; |
|
|
|
|
|
|
|
|
|
|
|
# we must have remote candidate and a pair |
|
|
|
|
|
my $cand = $self->{remote_peers}->{"UDP/$address/$port"}; |
|
|
|
|
|
$cand or die; |
|
|
|
|
|
my $pair = $self->get_pair($comp->{foundation}, $cand->{foundation}, $comp->{component}); |
|
|
|
|
|
$pair or die; |
|
|
|
|
|
$tid eq $pair->{transaction} or die; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("$pair->{foundation} succeeded\n"); |
|
|
|
|
|
$pair->{state} = 'succeeded'; |
|
|
|
|
|
|
|
|
|
|
|
# unfreeze other components |
|
|
|
|
|
my $parent_pair = $self->{candidate_pairs}->{$pair->{foundation}}; |
|
|
|
|
|
my $components = $parent_pair->{components}; |
|
|
|
|
|
my @frozen_pairs = grep {$_->{state} eq 'frozen'} @$components; |
|
|
|
|
|
for my $p (@frozen_pairs) { |
|
|
|
|
|
$self->debug("unfreezing $p->{local}->{port}\n"); |
|
|
|
|
|
$p->{state} = 'waiting'; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub integrity { |
|
|
|
|
|
my ($self, $attrs, $mtype, $tid, $pwd) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my $int_check = join('', @$attrs); |
|
|
|
|
|
$int_check = pack('nnNa12', $mtype, length($int_check) + 24, 0x2112A442, $tid) . $int_check; |
|
|
|
|
|
my $digest = hmac_sha1($int_check, $pwd); |
|
|
|
|
|
push(@$attrs, attr(0x0008, $digest)); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub fingerprint { |
|
|
|
|
|
my ($self, $attrs, $mtype, $tid) = @_; |
|
|
|
|
|
|
|
|
|
|
|
my $fp_check = join('', @$attrs); |
|
|
|
|
|
$fp_check = pack('nnNa12', $mtype, length($fp_check) + 8, 0x2112A442, $tid) . $fp_check; |
|
|
|
|
|
my $crc = crc32($fp_check); |
|
|
|
|
|
push(@$attrs, attr(0x8028, pack('N', ($crc ^ 0x5354554e)))); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub attr { |
|
|
|
|
|
my ($id, $data) = @_; |
|
|
|
|
|
my $len = length($data); |
|
|
|
|
|
my $padding = 4 - ($len % 4); |
|
|
|
|
|
$padding == 4 and $padding = 0; |
|
|
|
|
|
return pack('nn a*a*', $id, $len, $data, "\0" x $padding); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub stun_handler_SOFTWARE { |
|
|
|
|
|
my ($data, $out) = @_; |
|
|
|
|
|
return { name => 'software', data => $data }; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_USE_CANDIDATE { |
|
|
|
|
|
my ($data, $out) = @_; |
|
|
|
|
|
return { name => 'use' }; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_ICE_CONTROLLED { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'controlled' }; |
|
|
|
|
|
$out->{controlled} = 1; |
|
|
|
|
|
($out->{tie_breaker_hi}, $out->{tie_breaker_lo}) = unpack('NN', $data); |
|
|
|
|
|
$out->{tie_breaker} = i64from32($out->{tie_breaker_hi}, $out->{tie_breaker_lo}); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_ICE_CONTROLLING { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'controlling' }; |
|
|
|
|
|
$out->{controlling} = 1; |
|
|
|
|
|
($out->{tie_breaker_hi}, $out->{tie_breaker_lo}) = unpack('NN', $data); |
|
|
|
|
|
$out->{tie_breaker} = i64from32($out->{tie_breaker_hi}, $out->{tie_breaker_lo}); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_USERNAME { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'username' }; |
|
|
|
|
|
$data =~ /^(.*):(.*)$/ or die; |
|
|
|
|
|
$out->{my_ufrag} = $1; |
|
|
|
|
|
$out->{other_ufrag} = $2; |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_PRIORITY { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'priority' }; |
|
|
|
|
|
($out->{priority}) = unpack('N', $data); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_MESSAGE_INTEGRITY { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'integrity' }; |
|
|
|
|
|
$out->{digest} = $data; |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_FINGERPRINT { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'fingerprint' }; |
|
|
|
|
|
($out->{crc}) = unpack('N', $data); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_ERROR_CODE { |
|
|
|
|
|
my ($data) = @_; |
|
|
|
|
|
my $out = { name => 'error' }; |
|
|
|
|
|
my ($code, $msg) = unpack('Na*', $data); |
|
|
|
|
|
$out->{msg} = $msg; |
|
|
|
|
|
$out->{code} = (($code & 0x700) >> 8) * 100 + ($code & 0x0ff); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
sub stun_handler_XOR_MAPPED_ADDRESS { |
|
|
|
|
|
my ($data, $tid) = @_; |
|
|
|
|
|
my $out = { name => 'address' }; |
|
|
|
|
|
if (length($data) == 8) { |
|
|
|
|
|
my ($fam, $port, $addr) = unpack('nna4', $data); |
|
|
|
|
|
$fam == 1 or die; |
|
|
|
|
|
$out->{af} = &AF_INET; |
|
|
|
|
|
$out->{port} = $port ^ 0x2112; |
|
|
|
|
|
$out->{address} = $addr ^ "\x21\x12\xa4\x42"; |
|
|
|
|
|
} |
|
|
|
|
|
elsif (length($data) == 20) { |
|
|
|
|
|
my ($fam, $port, $addr) = unpack('nna16', $data); |
|
|
|
|
|
$fam == 2 or die; |
|
|
|
|
|
$out->{af} = &AF_INET6; |
|
|
|
|
|
$out->{port} = $port ^ 0x2112; |
|
|
|
|
|
$out->{address} = $addr ^ ("\x21\x12\xa4\x42" . $tid); |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
die; |
|
|
|
|
|
} |
|
|
|
|
|
$out->{address} = inet_ntop($out->{af}, $out->{address}); |
|
|
|
|
|
return $out; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub timer { |
|
|
|
|
|
my ($self) = @_; |
|
|
|
|
|
my $now = time(); |
|
|
|
|
|
$now - $self->{last_timer} < 0.02 and return; |
|
|
|
|
|
$self->{last_timer} = $now; |
|
|
|
|
|
|
|
|
|
|
|
# run checks |
|
|
|
|
|
|
|
|
|
|
|
defined($self->{other_ufrag}) && defined($self->{other_pwd}) or return; # not enough info |
|
|
|
|
|
|
|
|
|
|
|
if (my $pair = shift(@{$self->{triggered_checks}})) { |
|
|
|
|
|
$self->debug("$pair->{foundation} - running triggered check\n"); |
|
|
|
|
|
$self->run_check($pair); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
# get all component pairs, sort by their priority and run check for the highest waiting one |
|
|
|
|
|
|
|
|
|
|
|
my @candidate_pairs = values(%{$self->{candidate_pairs}}); |
|
|
|
|
|
my @component_pairs = map {@{$_->{components}}} @candidate_pairs; |
|
|
|
|
|
my @sorted_pairs = $self->sort_pairs(\@component_pairs); |
|
|
|
|
|
my @waiting_pairs = grep {$_->{state} eq 'waiting'} @sorted_pairs; |
|
|
|
|
|
|
|
|
|
|
|
if (my $pair = shift(@waiting_pairs)) { |
|
|
|
|
|
$self->debug("$pair->{foundation} - running scheduled check (waiting state)\n"); |
|
|
|
|
|
$self->run_check($pair); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub sort_pairs { |
|
|
|
|
|
my ($self, $pair_list) = @_; |
|
|
|
|
|
return sort {$self->pair_priority($a) <=> $self->pair_priority($b)} @$pair_list; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub trigger_check { |
|
|
|
|
|
my ($self, $pair) = @_; |
|
|
|
|
|
$self->debug("$pair->{foundation} - trigger check\n"); |
|
|
|
|
|
if ($pair->{state} eq 'succeeded') { |
|
|
|
|
|
$self->debug("$pair->{foundation} - already succeeded\n"); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
if ($pair->{state} eq 'in progress') { |
|
|
|
|
|
$self->cancel_check($pair); |
|
|
|
|
|
} |
|
|
|
|
|
push(@{$self->{triggered_checks}}, $pair); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub run_check { |
|
|
|
|
|
my ($self, $pair) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$pair->{state} eq 'in progress' and return; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("$pair->{foundation} - running check\n"); |
|
|
|
|
|
$pair->{state} = 'in progress'; |
|
|
|
|
|
$pair->{transaction} = random_string(12); |
|
|
|
|
|
$self->send_check($pair); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub cancel_check { |
|
|
|
|
|
my ($self, $pair) = @_; |
|
|
|
|
|
$self->debug("$pair->{foundation} - canceling existing check $pair->{transaction}\n"); |
|
|
|
|
|
$pair->{previous_transactions}->{$pair->{transaction}} = 1; |
|
|
|
|
|
delete $pair->{transaction}; |
|
|
|
|
|
$pair->{state} = 'waiting'; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
sub send_check { |
|
|
|
|
|
my ($self, $pair) = @_; |
|
|
|
|
|
|
|
|
|
|
|
$self->debug("$pair->{foundation} - sending check $pair->{transaction}\n"); |
|
|
|
|
|
|
|
|
|
|
|
$pair->{last_transmit} = time(); |
|
|
|
|
|
my $local_comp = $pair->{local}; |
|
|
|
|
|
my $remote_comp = $pair->{remote}; |
|
|
|
|
|
my $local_cand = $self->{candidates}->{$local_comp->{foundation}}; |
|
|
|
|
|
|
|
|
|
|
|
my $attrs = []; |
|
|
|
|
|
unshift(@$attrs, attr(0x8022, 'perl:ICE.pm')); |
|
|
|
|
|
my $hexbrk = $self->{tie_breaker}->as_hex(); |
|
|
|
|
|
$hexbrk =~ s/^0x// or die; |
|
|
|
|
|
$hexbrk = ('0' x (16 - length($hexbrk))) . $hexbrk; |
|
|
|
|
|
unshift(@$attrs, attr($self->{controlling} ? 0x802a : 0x8029, pack('H*', $hexbrk))); |
|
|
|
|
|
unshift(@$attrs, attr(0x0024, pack('N', calc_priority('prflx', |
|
|
|
|
|
$local_cand->{preference}, $local_comp->{component})))); |
|
|
|
|
|
unshift(@$attrs, attr(0x0006, "$self->{other_ufrag}:$self->{my_ufrag}")); |
|
|
|
|
|
|
|
|
|
|
|
$self->integrity($attrs, 1, $pair->{transaction}, $self->{other_pwd}); |
|
|
|
|
|
$self->fingerprint($attrs, 1, $pair->{transaction}); |
|
|
|
|
|
|
|
|
|
|
|
my $packet = join('', @$attrs); |
|
|
|
|
|
$packet = pack('nnNa12', 1, length($packet), 0x2112A442, $pair->{transaction}) . $packet; |
|
|
|
|
|
$local_comp->{socket}->send($packet, 0, $remote_comp->{packed_peer}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
# XXX use multiple packages here for candidates, components and pairs |
|
|
|
|
|
|
|
|
|
|
|
1; |