File Coverage

blib/lib/Net/SIP/NATHelper/Base.pm
Criterion Covered Total %
statement 69 433 15.9
branch 0 102 0.0
condition 0 68 0.0
subroutine 23 60 38.3
pod 11 12 91.6
total 103 675 15.2


line stmt bran cond sub pod time code
1 4     4   522 use strict;
  4         9  
  4         104  
2 4     4   30 use warnings;
  4         8  
  4         222  
3              
4             ############################################################################
5             #
6             # NATHelper::Base
7             # Helper class for NAT of RTP connections
8             # - allocate sockets for rewriting SDP bodies
9             # - transfer data between sockets within sessions
10             # - close sessions
11             # - expire sockets and sessions on inactivity
12             #
13             ############################################################################
14              
15             #
16             # ---------------- Base ------------------------------------------------
17             # | | | | ...
18             # call-id
19             # |
20             # ---------- Call's -----------------------------------
21             # | | | | ...
22             # idfrom
23             # |
24             # ---------------------------------------------
25             # | | | | ...
26             # cseq
27             # |
28             # -----------------
29             # | | |
30             # | | socket_group_from: SocketGroup
31             # | |
32             # | socket_groups_to
33             # | |
34             # | |- idto: SocketGroup
35             # | |- idto: SocketGroup
36             # | |- idto: SocketGroup
37             # | |- idto: SocketGroup
38             # | |...
39             # |
40             # sessions
41             # |
42             # |- idto: Session containing 2 x SocketGroup
43             # |- idto: Session containing 2 x SocketGroup
44             # |...
45             #
46              
47              
48             package Net::SIP::NATHelper::Base;
49 4     4   21 use fields qw( calls max_sockets max_sockets_in_group socket_count group_count );
  4         8  
  4         27  
50              
51 4     4   335 use Net::SIP::Util ':all';
  4         7  
  4         653  
52 4     4   26 use Net::SIP::Debug;
  4         9  
  4         37  
53 4     4   31 use List::Util qw( first sum );
  4         8  
  4         222  
54 4     4   22 use Time::HiRes 'gettimeofday';
  4         8  
  4         22  
55 4     4   339 use Errno 'EMFILE';
  4         8  
  4         148  
56 4     4   19 use Socket;
  4         8  
  4         5751  
57              
58             ############################################################################
59             # create new Net::SIP::NATHelper::Base
60             # Args: ($class,%args);
61             # Returns: $self
62             ############################################################################
63             sub new {
64 0     0 1   my ($class,%args) = @_;
65             # Hash of Net::SIP::NATHelper::Call indexed by call-id
66 0           my $self = fields::new($class);
67             %$self = (
68             calls => {},
69             socket_count => 0,
70             group_count => 0,
71             max_sockets => delete $args{max_sockets},
72             max_sockets_in_group => delete $args{max_sockets_in_group},
73 0           );
74 0           return $self;
75             }
76              
77             ############################################################################
78             # create a new call - might be redefined in derived classes to use
79             # other call classes
80             # Args: ($self,$callid)
81             # $callid: call-id
82             # Returns: $call object
83             ############################################################################
84             sub create_call {
85 0     0 0   Net::SIP::NATHelper::Call->new($_[1])
86             }
87              
88             ############################################################################
89             # allocate new sockets for RTP
90             #
91             # Args: ($self,$callid,$cseq,$idfrom,$idto,$side,$addr,\@media)
92             # $callid: call-id
93             # $cseq: sequence number for cseq
94             # $idfrom: ID for from-side
95             # $idto: ID for to-side
96             # $side: 0 if SDP is from request, else 1
97             # $addr: IP where to create the new sockets
98             # \@media: media like returned from Net::SIP::SDP::get_media
99             #
100             # Returns: $media
101             # $media: \@list of [ip,base_port] of with the size of \@media
102             #
103             # Comment: if it fails () will be returned. In this cases the SIP packet
104             # should not be forwarded (dropped) thus causing a retransmit (for UDP)
105             # which will then cause another call to allocate_sockets and maybe this
106             # time we have enough resources
107             ############################################################################
108             sub allocate_sockets {
109 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
110 0           my $callid = shift;
111              
112 0   0       my $call = $self->{calls}{$callid}
113             ||= $self->create_call($callid);
114 0           return $call->allocate_sockets( $self,@_ );
115             }
116              
117              
118             ############################################################################
119             # activate session
120             # Args: ($self,$callid,$cseq,$idfrom,$idto;$param)
121             # $callid: call-id
122             # $cseq: sequence number for cseq
123             # $idfrom: ID for from-side
124             # $idto: ID for to-side
125             # $param: user defined param which gets returned from info_as_hash
126             # Returns: ($info,$duplicate)
127             # $info: hash from sessions info_as_hash
128             # $duplicate: TRUE if session was already created
129             # Comment: if it returns FALSE because it fails the SIP packet will not
130             # be forwarded. This is the case on retransmits of really old SIP
131             # packets where the session was already closed
132             ############################################################################
133             sub activate_session {
134 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
135 0           my $callid = shift;
136              
137 0           my $call = $self->{calls}{$callid};
138 0 0         unless ( $call ) {
139 0           DEBUG( 10,"tried to activate non-existing call $callid" );
140 0           return;
141             }
142 0           return $call->activate_session( @_ );
143             }
144              
145             ############################################################################
146             # close session(s)
147             # Args: ($self,$callid,$cseq,$idfrom,$idto)
148             # $callid: call-id
149             # $cseq: optional sequence number, only for CANCEL requests
150             # $idfrom: ID for from-side
151             # $idto: ID for to-side
152             # Returns: @session_info
153             # @session_info: list of hashes from session info_as_hash
154             # Comment: this SIP packet should be forwarded, even if the call
155             # is not known here, because it did not receive the response from
156             # the peer yet (e.g. was retransmit)
157             ############################################################################
158             sub close_session {
159 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
160 0           my $callid = shift;
161              
162 0           my $call = $self->{calls}{$callid};
163 0 0         unless ( $call ) {
164 0           DEBUG( 10,"tried to close non-existing call $callid" );
165 0           return;
166             }
167 0           return $call->close_session( @_ );
168             }
169              
170              
171             ############################################################################
172             # cleanup, e.g. delete expired sessions and unused socket groups
173             # Args: ($self,%args)
174             # %args: hash with the following data
175             # time: current time, will get from gettimeofday() if not given
176             # unused: seconds for timeout of sockets, which were never used in session
177             # defaults to 3 minutes
178             # active: seconds for timeout of sockets used in sessions, defaults to
179             # 30 seconds
180             # Returns: @expired
181             # @expired: list of infos about expired sessions using sessions info_as_hash
182             ############################################################################
183             sub expire {
184 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
185 0           my %args = @_;
186              
187 0   0       $args{time} ||= gettimeofday();
188 0   0       $args{unused} ||= 3*60; # unused sockets after 3 minutes
189 0   0       $args{active} ||= 30; # active sessions after 30 seconds
190 0           DEBUG( 100,"expire now=$args{time} unused=$args{unused} active=$args{active}" );
191 0           my @expired;
192 0           my $calls = $self->{calls};
193 0           foreach my $callid ( keys %$calls ) {
194 0           my $call = $calls->{$callid};
195 0           push @expired, $call->expire( %args );
196 0 0         if ( $call->is_empty ) {
197 0           DEBUG( 50,"remove call $callid" );
198 0           delete $calls->{$callid};
199             }
200             }
201 0           return @expired;
202             }
203              
204             ############################################################################
205             # collect the callbacks for all sessions in all calls
206             # Args: $self
207             # Returns: @callbacks, see *::Session::callbacks
208             ############################################################################
209             sub callbacks {
210 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
211 0           return map { $_->callbacks } values %{ $self->{calls} };
  0            
  0            
212             }
213              
214             ############################################################################
215             # run over all sessions and execute callback
216             # Args: $self;$callback
217             # $callback: callback, defaults to simply return the session
218             # Returns: @rv
219             # @rv: array with the return values of all callbacks together
220             ############################################################################
221             sub sessions {
222 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
223 0           my $callback = shift;
224 0   0 0     $callback ||= sub { return shift }; # default callback returns session
  0            
225 0           return map { $_->sessions( $callback ) } values %{ $self->{calls} };
  0            
  0            
226             }
227              
228             ############################################################################
229             # Dump debug information into string
230             # Args: $self
231             # Returns: $string
232             ############################################################################
233             sub dump {
234 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
235 0           my $result = "";
236 0           foreach ( values %{ $self->{calls} } ) {
  0            
237 0           $result.= $_->dump;
238             }
239 0           return $result;
240             }
241              
242             ############################################################################
243             # return number of reserved calls
244             # Args: $self
245             # Returns: $n
246             ############################################################################
247             sub number_of_calls {
248 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
249 0           return scalar( keys %{ $self->{calls} })
  0            
250             }
251              
252             ############################################################################
253             # get RTP sockets
254             # can be redefined to allow enforcing of resource limits, caching of
255             # sockets...
256             # right now creates fresh RTP sockets unless max_sockets is reached,
257             # in which case it returns () with $! set to EMFILE
258             # Args: ($self,$new_addr,$media)
259             # $new_addr: IP for new sockets
260             # $media: old media like given from Net::SIP::SDP::get_media
261             # Returns: \@new_media
262             # @new_media: list of [ addr,base_port,\@socks,\@targets]
263             # where addr and base_port are the address and base port for the new
264             # media, @socks the list of sockets and @targets the matching targets
265             # based on the original media
266             ############################################################################
267             sub get_rtp_sockets {
268 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
269 0           my ($new_addr,$media) = @_;
270 0           my @new_media;
271              
272 0           my $need_sockets = sum( map { $_->{range} } @$media );
  0            
273 0 0         if ( my $max = $self->{max_sockets_in_group} ) {
274 0 0         if ( $need_sockets > $max ) {
275 0           DEBUG( 1,"allocation of RTP sockets denied because max_sockets_in_group limit reached" );
276 0           $! = EMFILE;
277 0           return;
278             }
279             }
280              
281 0 0         if ( my $max = $self->{max_sockets} ) {
282 0 0         if ( $self->{socket_count} + $need_sockets > $max ) {
283 0           DEBUG( 1,"allocation of RTP sockets denied because max_sockets limit reached" );
284 0           $! = EMFILE;
285 0           return;
286             }
287             }
288              
289 0           foreach my $m (@$media) {
290 0           my ($addr,$port,$range) = @{$m}{qw/addr port range/};
  0            
291             # allocate new sockets
292 0           my ($new_port,@socks) = create_rtp_sockets( $new_addr,$range );
293 0 0         unless (@socks) {
294 0           DEBUG( 1,"allocation of RTP sockets failed: $!" );
295 0           return;
296             }
297              
298 0 0 0       if (!$port or $addr eq '0.0.0.0' or $addr eq '::') {
      0        
299             # RFC 3264 6.1 - stream marked as inactive
300 0           DEBUG(50,"inactive stream" );
301             push @new_media, [ $new_addr,0,\@socks,
302             # no target for socket on other side
303 0           [ map { undef } (0..$#socks) ]
  0            
304             ];
305             } else {
306 0           DEBUG( 100,"m_old=$addr $port/$range new_port=$new_port" );
307             push @new_media, [ $new_addr,$new_port,\@socks,
308             # target for sock on other side is original address
309 0           [ map { ip_parts2sockaddr($addr,$port+$_) } (0..$#socks) ]
  0            
310             ]
311             }
312             }
313              
314 0           $self->{socket_count} += $need_sockets;
315 0           $self->{group_count} ++;
316              
317 0           return \@new_media;
318             }
319              
320             ############################################################################
321             # free created RTP sockets
322             # Args: $self,$media
323             # $media: see return code from get_rtp_sockets
324             # Returns: NONE
325             ############################################################################
326             sub unget_rtp_sockets {
327 0     0 1   my Net::SIP::NATHelper::Base $self = shift;
328 0           my $media = shift;
329 0           $self->{group_count} --;
330 0           $self->{socket_count} -= sum( map { int(@{ $_->[2] }) } @$media );
  0            
  0            
331             }
332              
333             ############################################################################
334             ############################################################################
335             #
336             # Net::SIP::NATHelper::Call
337             # manages Call, e.g. for each active cseq for the same call-id
338             # it manages the Net::SIP::NATHelper::SocketGroup's and Net::SIP::NATHelper::Session's
339             #
340             ############################################################################
341             ############################################################################
342              
343             package Net::SIP::NATHelper::Call;
344 4     4   29 use fields qw( callid from );
  4         8  
  4         15  
345 4     4   256 use Hash::Util 'lock_keys';
  4         8  
  4         29  
346 4     4   234 use List::Util 'max';
  4         8  
  4         179  
347 4     4   60 use Net::SIP::Debug;
  4         22  
  4         18  
348 4     4   24 use Net::SIP::Util 'invoke_callback';
  4         14  
  4         7975  
349              
350             sub new {
351 0     0     my ($class,$callid) = @_;
352 0           my $self = fields::new($class);
353 0           %$self = (
354             callid => $callid,
355             from => {},
356             );
357 0           return $self;
358             }
359              
360             ############################################################################
361             # allocate sockets for rewriting SDP body
362             # Args: ($nathelper,$self,$cseq,$idfrom,$idto,$side,$addr,$media)
363             # Returns: $media
364             ############################################################################
365             sub allocate_sockets {
366 0     0     my Net::SIP::NATHelper::Call $self = shift;
367 0           my ($nathelper,$cseq,$idfrom,$idto,$side,$addr,$media) = @_;
368              
369             # find existing data for $idfrom,$cseq
370 0           my $cseq_data = $self->{from}{$idfrom};
371 0   0       my $data = $cseq_data && $cseq_data->{$cseq};
372              
373 0 0         if ( ! $data ) {
374             # if it is not known check if cseq is too small (retransmit of old packet)
375 0 0         if ( $cseq_data ) {
376 0           foreach ( keys %$cseq_data ) {
377 0 0         if ( $_ > $cseq ) {
378 0           DEBUG( 10,"retransmit? cseq $cseq is smaller than $_ in call $self->{callid}" );
379 0           return;
380             }
381             }
382             }
383              
384             # need new record
385 0   0       $cseq_data ||= $self->{from}{$idfrom} = {};
386 0           $data = $cseq_data->{$cseq} = {
387             socket_group_from => undef,
388             socket_groups_to => {}, # indexed by idto
389             sessions => {}, # indexed by idto
390             };
391 0           lock_keys( %$data );
392             }
393              
394             # if SocketGroup already exists return it's media
395             # otherwise try to create a new one
396             # if this fails return (), otherwise return media
397              
398 0           my $sgroup;
399 0 0         if ( $side == 0 ) { # FROM
400 0   0       $sgroup = $data->{socket_group_from} ||= do {
401 0           DEBUG( 10,"new socketgroup with idfrom $idfrom" );
402 0 0         Net::SIP::NATHelper::SocketGroup->new( $nathelper,$idfrom,$addr,$media )
403             || return;
404             };
405             } else {
406 0   0       $sgroup = $data->{socket_groups_to}{$idto} ||= do {
407 0           DEBUG( 10,"new socketgroup with idto $idto" );
408 0 0         Net::SIP::NATHelper::SocketGroup->new( $nathelper,$idto,$addr,$media )
409             || return;
410             };
411             }
412              
413 0           return $sgroup->get_media;
414             }
415              
416             ############################################################################
417             # activate session
418             # Args: ($self,$cseq,$idfrom,$idto;$param)
419             # Returns: ($info,$duplicate)
420             ############################################################################
421             sub activate_session {
422 0     0     my Net::SIP::NATHelper::Call $self = shift;
423 0           my ($cseq,$idfrom,$idto,$param) = @_;
424              
425 0           my $by_cseq = $self->{from}{$idfrom};
426 0   0       my $data = $by_cseq && $by_cseq->{$cseq};
427 0 0         unless ( $data ) {
428 0           DEBUG( 10,"tried to activate non-existing session $idfrom|$cseq in call $self->{callid}" );
429 0           return;
430             }
431              
432 0           my $sessions = $data->{sessions};
433 0 0         if ( my $sess = $sessions->{$idto} ) {
434             # exists already, maybe retransmit of ACK
435 0           return ( $sess->info_as_hash( $self->{callid},$cseq ), 1 );
436             }
437              
438 0           my $gfrom = $data->{socket_group_from};
439 0           my $gto = $data->{socket_groups_to}{$idto};
440 0 0 0       if ( !$gfrom || !$gto ) {
441 0           DEBUG( 50,"session $self->{callid},$cseq $idfrom -> $idto not complete " );
442 0           return;
443             }
444              
445 0           my $sess = $sessions->{$idto} = $self->create_session( $gfrom,$gto,$param );
446 0           DEBUG( 10,"new session {$sess->{id}} $self->{callid},$cseq $idfrom -> $idto" );
447              
448 0           return ( $sess->info_as_hash( $self->{callid},$cseq ), 0 );
449             }
450              
451             ############################################################################
452             # create Session object
453             # Args: ($self,$gfrom,$gto,$param)
454             # $gfrom: socket group on from-side
455             # $gto: socket group on to-side
456             # $param: optional session parameter (see Base::activate_session)
457             # Reuturns: $session object
458             ############################################################################
459             sub create_session {
460 0     0     shift;
461 0           return Net::SIP::NATHelper::Session->new(@_)
462             }
463              
464             ############################################################################
465             # close session
466             # Args: ($self,$cseq,$idfrom,$idto)
467             # $cseq: optional sequence number, only for CANCEL requests
468             # Returns: @session_info
469             # @session_info: list of infos of all closed sessions, info is hash with
470             # callid,cseq,idfrom,idto,from,to,bytes_from,bytes_to
471             ############################################################################
472             sub close_session {
473 0     0     my Net::SIP::NATHelper::Call $self = shift;
474 0           my ($cseq,$idfrom,$idto) = @_;
475              
476             #DEBUG( 100,$self->dump );
477              
478 0           my @info;
479 0 0         if ( $cseq ) {
480             # close initiated by CANCEL
481 0           my $data = $self->{from}{$idfrom};
482 0   0       $data = $data && $data->{$cseq};
483 0 0 0       my $sess = $data && delete( $data->{sessions}{$idto} ) or do {
484 0           DEBUG( 10,"tried to CANCEL non existing session in $self->{callid}|$cseq" );
485 0           return;
486             };
487 0           push @info, $sess->info_as_hash( $self->{callid},$cseq );
488 0           DEBUG( 10,"close session {$sess->{id}} $self->{callid}|$cseq $idto,$idfrom success" );
489              
490             } else {
491             # close from BYE (which has different cseq then the INVITE)
492             # need to close all sessions between idfrom and idto, because BYE could
493             # originate by UAC or UAS
494 0           foreach my $pair ( [ $idfrom,$idto ],[ $idto,$idfrom ] ) {
495 0           my ($from,$to) = @$pair;
496 0   0       my $by_cseq = $self->{from}{$from} || next;
497              
498 0           foreach my $cseq ( keys %$by_cseq ) {
499 0   0       my $sess = delete $by_cseq->{$cseq}{sessions}{$to} || next;
500 0           push @info, $sess->info_as_hash( $self->{callid},$cseq );
501 0           DEBUG( 10,"close session {$sess->{id}} $self->{callid}|$cseq $idto,$idfrom " );
502             }
503             }
504 0 0         unless (@info) {
505 0           DEBUG( 10,"tried to BYE non existing session in $self->{callid}" );
506 0           return;
507             }
508 0           DEBUG( 10,"close sessions $self->{callid} $idto,$idfrom success" );
509             }
510 0           return @info;
511             }
512              
513             ############################################################################
514             # expire call, e.g. inactive sessions, unused socketgroups...
515             # Args: ($self,%args)
516             # %args: see *::Base::expire
517             # Returns: @expired
518             # @expired: list of infos about expired sessions containing, see
519             # close_session
520             ############################################################################
521             sub expire {
522 0     0     my Net::SIP::NATHelper::Call $self = shift;
523 0           my %args = @_;
524              
525 0           my $expire_unused = $args{time} - $args{unused};
526 0           my $expire_active = $args{time} - $args{active};
527              
528 0           my @expired;
529             my %active_pairs; # mapping [idfrom,idto]|[idto,idfrom] -> session.created
530 0           my $need_next_pass;
531 0           my $by_from = $self->{from};
532              
533 0           for my $pass (1,2) {
534 0           while ( my ($idfrom,$by_cseq) = each %$by_from ) {
535              
536             # start with highest cseq so that we hopefully need 2 passes
537             # for expire session which got replaced by new ones
538 0           my @cseq = sort { $b <=> $a } keys %$by_cseq;
  0            
539 0           foreach my $cseq ( @cseq ) {
540 0           my $data = $by_cseq->{$cseq};
541              
542             # drop inactive sessions
543 0           my $sessions = $data->{sessions};
544 0           foreach my $idto ( keys %$sessions ) {
545 0           my $sess = $sessions->{$idto};
546 0           my $lastmod = max($sess->lastmod,$sess->{created});
547 0 0 0       if ( $lastmod < $expire_active ) {
    0 0        
548 0           DEBUG( 10,"expired session {$sess->{id}} $cseq|$idfrom|$idto because lastmod($lastmod) < active($expire_active)" );
549 0           my $sess = delete $sessions->{$idto};
550 0           push @expired, $sess->info_as_hash( $self->{callid}, $cseq, reason => 'expired' );
551              
552             } elsif ( my $created = max(
553             $active_pairs{ "$idfrom\0$idto" } || 0,
554             $active_pairs{ "$idto\0$idfrom" } || 0
555             ) ) {
556 0 0         if ( $created > $sess->{created} ) {
    0          
557 0           DEBUG( 10,"removed session {$sess->{id}} $cseq|$idfrom|$idto because there is newer session" );
558 0           my $sess = delete $sessions->{$idto};
559 0           push @expired, $sess->info_as_hash( $self->{callid}, $cseq, reason => 'replaced' );
560             } elsif ( $created < $sess->{created} ) {
561             # probably a session in the other direction has started
562 0           DEBUG( 100,"there is another session with created=$created which should be removed in next pass" );
563 0           $active_pairs{ "$idfrom\0$idto" } = $sess->{created};
564 0           $need_next_pass = 1
565             }
566             } else {
567             # keep session
568 0           DEBUG( 100,"session {$sess->{id}} $idfrom -> $idto created=$sess->{created} stays active in pass#$pass" );
569 0           $active_pairs{ "$idfrom\0$idto" } = $sess->{created};
570             }
571             }
572              
573             # delete socketgroups, which are not used in sessions and which
574             # are expired
575             # use string representation as key for comparison
576 0           my %used;
577 0           foreach ( values %$sessions ) {
578 0           $used{ $_->{sfrom} }++;
579 0           $used{ $_->{sto} }++;
580             }
581              
582 0           my $groups = $data->{socket_groups_to};
583 0           my %expired_sg;
584 0           my @v = values(%$groups);
585 0 0         push @v,$data->{socket_group_from} if $data->{socket_group_from};
586 0           foreach my $v ( @v ) {
587 0 0         next if $used{ $v }; # used in not expired session
588 0           my $lastmod = $v->{lastmod};
589 0 0         if ( ! $lastmod ) {
    0          
590             # was never used
591 0 0         if ( $v->{created} < $expire_unused ) {
592 0           DEBUG( 10,"expired socketgroup $v->{id} because created($v->{created}) < unused($expire_unused)" );
593 0           $expired_sg{$v} = 1;
594             }
595             } elsif ( $lastmod < $expire_active ) {
596 0           DEBUG( 10,"expired socketgroup $v->{id} because lastmod($lastmod) < active($expire_active)" );
597 0           $expired_sg{$v} = 1;
598             }
599             }
600              
601             $data->{socket_group_from} = undef if %expired_sg
602 0 0 0       and delete( $expired_sg{ $data->{socket_group_from} } );
603 0 0         if ( %expired_sg ) {
604 0           foreach my $id (keys(%$groups)) {
605 0 0         delete $groups->{$id} if delete $expired_sg{$groups->{$id}};
606 0 0         %expired_sg || last;
607             }
608             }
609             }
610             }
611              
612             # only run again if needed
613 0 0         $need_next_pass || last;
614 0           $need_next_pass = 0;
615 0           DEBUG( 100,'need another pass' );
616             }
617 0           return @expired;
618             }
619              
620             ############################################################################
621             # check if empty, e.g. no more socket groups on the call
622             # Args: $self
623             # Returns: TRUE if empty
624             ############################################################################
625             sub is_empty {
626 0     0     my Net::SIP::NATHelper::Call $self = shift;
627 0           my $by_from = $self->{from};
628 0           foreach my $idfrom ( keys %$by_from ) {
629 0           my $by_cseq = $by_from->{$idfrom};
630 0           foreach my $cseq ( keys %$by_cseq ) {
631 0           my $data = $by_cseq->{$cseq};
632 0 0 0       if ( ! %{ $data->{socket_groups_to}} && ! $data->{socket_group_from} ) {
  0            
633 0           DEBUG( 100,"deleted unused cseq $cseq in $self->{callid}|$idfrom" );
634 0           delete $by_cseq->{$cseq};
635             }
636             }
637 0 0         if ( ! %$by_cseq ) {
638 0           DEBUG( 100,"deleted unused idfrom $idfrom in $self->{callid}" );
639 0           delete $by_from->{$idfrom};
640             }
641             }
642 0 0         return %$by_from ? 0:1;
643             }
644              
645             ############################################################################
646             # collect the callbacks for all sessions within the call
647             # Args: $self
648             # Returns: @callbacks, see Net::SIP::NATHelper::Session::callbacks
649             ############################################################################
650             sub callbacks {
651 0     0     my Net::SIP::NATHelper::Call $self = shift;
652 0           my @cb;
653 0           my $by_from = $self->{from};
654 0           foreach my $by_cseq ( values %$by_from ) {
655 0           foreach my $data ( values %$by_cseq ) {
656 0           push @cb, map { $_->callbacks } values %{ $data->{sessions} };
  0            
  0            
657             }
658             }
659 0           return @cb;
660             }
661              
662             ############################################################################
663             # run over all session and execte callback
664             # Args: $self,$callback
665             # Returns: @rv
666             # @rv: results of all callback invocations together
667             ############################################################################
668             sub sessions {
669 0     0     my Net::SIP::NATHelper::Call $self = shift;
670 0           my $callback = shift;
671 0           my $by_from = $self->{from};
672 0           my @rv;
673 0           foreach my $by_cseq ( values %$by_from ) {
674 0           foreach my $data ( values %$by_cseq ) {
675 0           push @rv, map { invoke_callback($callback,$_) }
676 0           values %{ $data->{sessions} };
  0            
677             }
678             }
679 0           return @rv;
680             }
681              
682             ############################################################################
683             # Dump debug information into string
684             # Args: $self
685             # Returns: $string
686             ############################################################################
687             sub dump {
688 0     0     my Net::SIP::NATHelper::Call $self = shift;
689 0           my $result = "-- DUMP of call $self->{callid} --\n";
690 0           my $by_from = $self->{from};
691 0           foreach my $idfrom ( sort keys %$by_from ) {
692 0           my $by_cseq = $by_from->{$idfrom};
693 0           foreach ( sort { $a <=> $b } keys %$by_cseq ) {
  0            
694 0           $result.= "-- Socket groups in $idfrom|$_ --\n";
695 0           my $sgroups = $by_cseq->{$_}{socket_groups_to};
696 0           my $sf = $by_cseq->{$_}{socket_group_from};
697 0 0         $result .= $sf->dump if $sf;
698 0           foreach ( sort keys %$sgroups ) {
699 0           $result.= $sgroups->{$_}->dump;
700             }
701 0           $result.= "-- Sessions in $idfrom|$_ --\n";
702 0           my $sessions = $by_cseq->{$_}{sessions};
703 0           foreach ( sort keys %$sessions ) {
704 0           $result.= $sessions->{$_}->dump;
705             }
706             }
707             }
708 0           return $result;
709             }
710              
711              
712             ############################################################################
713             ############################################################################
714             #
715             # Net::SIP::NATHelper::Session
716             # each session consists of two Net::SIP::NATHelper::SocketGroup's and the data
717             # are transferred between these groups
718             #
719             ############################################################################
720             ############################################################################
721              
722             package Net::SIP::NATHelper::Session;
723 4     4   36 use fields qw( sfrom sto created bytes_from bytes_to callbacks id param );
  4         10  
  4         17  
724 4     4   371 use Net::SIP::Debug;
  4         8  
  4         22  
725 4     4   26 use List::Util 'max';
  4         8  
  4         216  
726 4     4   24 use Net::SIP::Util ':all';
  4         6  
  4         637  
727 4     4   24 use Time::HiRes 'gettimeofday';
  4         8  
  4         15  
728              
729             # increased for each new session
730             my $session_id = 0;
731              
732             ############################################################################
733             # create new Session between two SocketGroup's
734             # Args: ($class,$socketgroup_from,$socketgroup_to;$param)
735             # Returns: $self
736             ############################################################################
737             sub new {
738 0     0     my ($class,$sfrom,$sto,$param) = @_;
739 0           my $self = fields::new( $class );
740              
741             # sanity check that both use the same number of sockets
742 0 0         if ( @{ $sfrom->get_socks } != @{ $sto->get_socks } ) {
  0            
  0            
743 0           DEBUG( 1,"different number of sockets in request and response" );
744 0           return;
745             }
746              
747 0           %$self = (
748             sfrom => $sfrom,
749             sto => $sto,
750             created => scalar( gettimeofday() ),
751             bytes_from => 0,
752             bytes_to => 0,
753             callbacks => undef,
754             param => $param,
755             id => ++$session_id,
756             );
757 0           return $self;
758             }
759              
760             ############################################################################
761             # returns session info as hash
762             # Args: ($self,$callid,$cseq,%more)
763             # %more: hash with more key,values to put into info
764             # Returns: %session_info
765             # %session_info: hash with callid,cseq,idfrom,idto,from,to,
766             # bytes_from,bytes_to,sessionid and %more
767             ############################################################################
768             sub info_as_hash {
769 0     0     my Net::SIP::NATHelper::Session $self = shift;
770 0           my ($callid,$cseq,%more) = @_;
771              
772             my $from = join( ",", map {
773 0           "$_->{addr}:$_->{port}/$_->{range}"
774 0           } @{ $self->{sfrom}{orig_media} } );
  0            
775              
776             my $to = join( ",", map {
777 0           "$_->{addr}:$_->{port}/$_->{range}"
778 0           } @{ $self->{sto}{orig_media} } );
  0            
779              
780             return {
781             callid => $callid,
782             cseq => $cseq,
783             idfrom => $self->{sfrom}{id},
784             idto => $self->{sto}{id},
785             from => $from,
786             to => $to,
787             bytes_from => $self->{bytes_from},
788             bytes_to => $self->{bytes_to},
789             created => $self->{created},
790             sessionid => $self->{id},
791             param => $self->{param},
792 0           %more,
793             }
794             }
795              
796             ############################################################################
797             # return time of last modification, e.g. maximum of lastmod of both
798             # socketgroups
799             # Args: $self
800             # Returns: $lastmod
801             ############################################################################
802             sub lastmod {
803 0     0     my Net::SIP::NATHelper::Session $self = shift;
804 0           return max( $self->{sfrom}{lastmod}, $self->{sto}{lastmod} );
805             }
806              
807             ############################################################################
808             # return all [ socket, callback,cbid ] tuples for the session
809             # cbid is uniq for each callback and can be used to detect, which callbacks
810             # changed compared to the last call
811             # Args: $self
812             # Returns: @callbacks
813             ############################################################################
814              
815             my $callback_id = 0; # uniq id for each callback
816             sub callbacks {
817 0     0     my Net::SIP::NATHelper::Session $self = shift;
818              
819 0           my $callbacks = $self->{callbacks};
820 0 0         return @$callbacks if $callbacks; # already computed
821              
822             # data received on sockets in $sfrom will be forwarded to the original
823             # target from $sfrom using the matching socket from $sto and the other
824             # way around.
825             # This means we do symetric RTP in all cases
826              
827 0           my $sfrom = $self->{sfrom};
828 0           my $sockets_from = $sfrom->get_socks;
829 0           my $targets_from = $sfrom->get_targets;
830              
831 0           my $sto = $self->{sto};
832 0           my $sockets_to = $sto->get_socks;
833 0           my $targets_to = $sto->get_targets;
834              
835 0           my $fwd_data = $self->can('forward_data');
836              
837 0           my @cb;
838 0           for( my $i=0;$i<@$sockets_from;$i++ ) {
839             # If we detect, that the peer does symmetric RTP we connect the
840             # socket and set the addr to undef to make sure that we use send
841             # and not sendto when forwarding the data
842 0           my $recvaddr = $targets_to->[$i];
843 0           my $dstaddr = $targets_from->[$i];
844              
845             $dstaddr && push @cb, [
846             $sockets_from->[$i],
847             [
848             $fwd_data,
849             $sockets_from->[$i], # read data from socket FROM(nat)
850             $sockets_to->[$i], # forward them using socket TO(nat)
851             \$recvaddr,\$dstaddr, # will be set to undef once connected
852             $sfrom, # call $sfrom->didit
853             \$self->{bytes_to}, # to count bytes coming from 'to'
854             $self->{id}, # for debug messages
855 0 0         ],
856             ++$callback_id
857             ];
858              
859             $recvaddr && push @cb, [
860             $sockets_to->[$i],
861             [
862             $fwd_data,
863             $sockets_to->[$i], # read data from socket TO(nat)
864             $sockets_from->[$i], # forward data using socket FROM(nat)
865             \$dstaddr,\$recvaddr, # will be set to undef once connected
866             $sto, # call $sto->didit
867             \$self->{bytes_from}, # to count bytes coming from 'from'
868             $self->{id}, # for debug messages
869 0 0         ],
870             ++$callback_id
871             ];
872             }
873 0           $self->{callbacks} = \@cb; # cache
874 0           return @cb;
875             }
876              
877             ############################################################################
878             # function used for forwarding data in callbacks()
879             ############################################################################
880             sub forward_data {
881 0     0     my ($read_socket,$write_socket,$rfrom,$rto,$group,$bytes,$id) = @_;
882 0   0       my $peer = recv( $read_socket, my $buf,2**16,0 ) || do {
883             DEBUG( 10,"recv data failed: $!" );
884             return;
885             };
886              
887 0     0     my $name = sub { ip_sockaddr2string(shift) };
  0            
888              
889 0 0 0       if ( ! $$bytes ) {
    0          
890 0 0         if ( $peer eq $$rfrom ) {
891 0           DEBUG( 10,"peer ".$name->($peer).
892             " uses symmetric RTP, connecting sockets");
893 0 0         $$rfrom = undef if connect($read_socket,$peer);
894             } else {
895             # set rfrom to peer for later checks
896 0           $$rfrom = $peer;
897             }
898             } elsif ( $$rfrom && $peer ne $$rfrom ) {
899             # the previous packet was from another peer, ignore this data
900 0           DEBUG( 10,"{$id} ignoring unexpected data from %s on %s, expecting data from %s instead",
901             $name->($peer), $name->(getsockname($read_socket)),$name->($$rfrom));
902             }
903              
904 0           my $l = length($buf);
905 0           $$bytes += $l;
906 0           $group->didit($l);
907              
908 0 0         if ( $$rto ) {
909 0 0         send( $write_socket, $buf,0, $$rto ) || do {
910 0           DEBUG( 10,"send data failed: $!" );
911 0           return;
912             };
913 0           DEBUG( 50,"{$id} transferred %d bytes on %s via %s to %s",
914             length($buf), $name->( getsockname($read_socket )),
915             $name->(getsockname( $write_socket )),$name->($$rto));
916             } else {
917             # using connected socket
918 0 0         send( $write_socket, $buf,0 ) || do {
919 0           DEBUG( 10,"send data failed: $!" );
920 0           return;
921             };
922 0           DEBUG( 50,"{$id} transferred %d bytes on %s via %s to %s",
923             length($buf), $name->( getsockname($read_socket )),
924             $name->(getsockname( $write_socket )),
925             $name->(getpeername( $write_socket )));
926             }
927             }
928              
929              
930             ############################################################################
931             # Dump debug information into string
932             # Args: $self
933             # Returns: $string
934             ############################################################################
935             sub dump {
936 0     0     my Net::SIP::NATHelper::Session $self = shift;
937             return "{$self->{id}}".
938             ( $self->{sfrom} && $self->{sfrom}{id} || 'NO.SFROM' ).",".
939 0   0       ( $self->{sto} && $self->{sto}{id} || 'NO.STO' )."\n";
      0        
940             }
941              
942             ############################################################################
943             ############################################################################
944             #
945             # Net::SIP::NATHelper::SocketGroup
946             # manages groups of sockets created from an SDP body
947             # manages the local (NAT) sockets and the original targets from the SDP
948             #
949             ############################################################################
950             ############################################################################
951              
952             package Net::SIP::NATHelper::SocketGroup;
953 4     4   3946 use fields qw( id created lastmod new_media orig_media nathelper );
  4         9  
  4         16  
954 4     4   320 use Net::SIP::Debug;
  4         6  
  4         22  
955 4     4   38 use Time::HiRes 'gettimeofday';
  4         8  
  4         24  
956 4     4   301 use Socket;
  4         8  
  4         3334  
957              
958             ############################################################################
959             # create new socket group based on the original media and a local address
960             # Args: ($class,$nathelper,$id,$new_addr,$media)
961             # Returns: $self|()
962             # Comment: () will be returned if allocation of sockets fails
963             ############################################################################
964             sub new {
965 0     0     my ($class,$nathelper,$id,$new_addr,$media) = @_;
966 0 0         my $new_media = $nathelper->get_rtp_sockets( $new_addr,$media )
967             or return;
968              
969 0           my $self = fields::new($class);
970 0           %$self = (
971             nathelper => $nathelper,
972             id => $id,
973             orig_media => [ @$media ],
974             new_media => $new_media,
975             lastmod => 0,
976             created => scalar( gettimeofday() ),
977             );
978 0           return $self;
979             }
980              
981             ############################################################################
982             # give allocated sockets back to NATHelper
983             ############################################################################
984             sub DESTROY {
985 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
986             ($self->{nathelper} || return )->unget_rtp_sockets( $self->{new_media} )
987 0   0       }
988              
989              
990             ############################################################################
991             # updates timestamp of last modification, used in expiring
992             # Args: ($self)
993             # Returns: NONE
994             ############################################################################
995             sub didit {
996 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
997 0           $self->{lastmod} = gettimeofday();
998             }
999              
1000             ############################################################################
1001             # returns \@list of media [ip,port,range] in group
1002             # Args: $self
1003             # Returns: \@media
1004             ############################################################################
1005             sub get_media {
1006 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1007             my @media = map { [
1008             $_->[0], # addr
1009             $_->[1], # base port
1010 0           int(@{$_->[2]}) # range, e.g number of sockets
  0            
1011 0           ]} @{ $self->{new_media} };
  0            
1012 0           return \@media;
1013             }
1014              
1015             ############################################################################
1016             # returns \@list of sockets in group
1017             # Args: $self
1018             # Returns: \@sockets
1019             ############################################################################
1020             sub get_socks {
1021 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1022 0           return [ map { @{$_->[2]} } @{$self->{new_media}} ];
  0            
  0            
  0            
1023             }
1024              
1025             ############################################################################
1026             # returns \@list of the original targets in group
1027             # Args: $self
1028             # Returns: \@targets
1029             ############################################################################
1030             sub get_targets {
1031 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1032 0           return [ map { @{$_->[3]} } @{$self->{new_media}} ];
  0            
  0            
  0            
1033             }
1034              
1035             ############################################################################
1036             # Dump debug information into string
1037             # Args: $self
1038             # Returns: $string
1039             ############################################################################
1040             sub dump {
1041 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1042             my $result = $self->{id}." >> ".join( ' ',
1043 0           map { "$_->[0]:$_->[1]/$_->[2]" }
1044 0           @{$self->get_media} ).
  0            
1045             "\n";
1046 0           return $result;
1047             }
1048              
1049             1;