File Coverage

blib/lib/Net/SIP/NATHelper/Base.pm
Criterion Covered Total %
statement 69 449 15.3
branch 0 110 0.0
condition 0 69 0.0
subroutine 23 60 38.3
pod 11 12 91.6
total 103 700 14.7


line stmt bran cond sub pod time code
1 4     4   476 use strict;
  4         7  
  4         101  
2 4     4   16 use warnings;
  4         8  
  4         175  
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   18 use fields qw( calls max_sockets max_sockets_in_group socket_count group_count );
  4         7  
  4         22  
50              
51 4     4   325 use Net::SIP::Util ':all';
  4         9  
  4         652  
52 4     4   24 use Net::SIP::Debug;
  4         9  
  4         21  
53 4     4   24 use List::Util qw( first sum );
  4         6  
  4         217  
54 4     4   31 use Time::HiRes 'gettimeofday';
  4         7  
  4         29  
55 4     4   357 use Errno 'EMFILE';
  4         15  
  4         147  
56 4     4   20 use Socket;
  4         9  
  4         5641  
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   27 use fields qw( callid from );
  4         8  
  4         22  
345 4     4   248 use Hash::Util 'lock_keys';
  4         8  
  4         28  
346 4     4   244 use List::Util 'max';
  4         8  
  4         185  
347 4     4   22 use Net::SIP::Debug;
  4         19  
  4         18  
348 4     4   23 use Net::SIP::Util 'invoke_callback';
  4         18  
  4         8285  
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 orr ACK to 401
481 0           my $data = $self->{from}{$idfrom};
482 0   0       $data = $data && $data->{$cseq};
483 0 0 0       if (my $sess = $data && delete( $data->{sessions}{$idto} )) {
484 0           push @info, $sess->info_as_hash( $self->{callid},$cseq );
485 0           DEBUG( 10,"close session {$sess->{id}} $self->{callid}|$cseq $idfrom -> $idto success" );
486             } else {
487 0           DEBUG( 10,"tried to CANCEL non existing session in $self->{callid}|$cseq" );
488             }
489 0 0 0       if ($data && !%{$data->{sessions}}) {
  0            
490 0           %{$data->{socket_groups_to}} = ();
  0            
491 0           $data->{socket_group_from} = undef;
492 0           DEBUG( 10,"cancel sessions $self->{callid}|$cseq $idfrom -> $idfrom - no more sessions" );
493 0           delete $self->{from}{$idfrom}{$cseq};
494             }
495              
496             } else {
497             # close from BYE (which has different cseq then the INVITE)
498             # need to close all sessions between idfrom and idto, because BYE could
499             # originate by UAC or UAS
500 0           foreach my $pair ( [ $idfrom,$idto ],[ $idto,$idfrom ] ) {
501 0           my ($from,$to) = @$pair;
502 0   0       my $by_cseq = $self->{from}{$from} || next;
503              
504 0           my @del_cseq;
505 0           while (my ($cseq,$data) = each %$by_cseq) {
506 0 0         if (my $sess = delete $data->{sessions}{$to}) {
507 0           push @info, $sess->info_as_hash( $self->{callid},$cseq );
508 0           DEBUG( 10,"close session {$sess->{id}} $self->{callid}|$cseq $idfrom -> $idto " );
509             }
510 0 0         if (!%{$data->{sessions}}) {
  0            
511 0           %{$data->{socket_groups_to}} = ();
  0            
512 0           $data->{socket_group_from} = undef;
513 0           DEBUG( 10,"bye sessions $self->{callid}|$cseq $idfrom -> $idto - no more sessions" );
514 0           push @del_cseq, $cseq;
515             }
516             }
517 0 0         delete @{$by_cseq}{@del_cseq} if @del_cseq;
  0            
518             }
519 0 0         unless (@info) {
520 0           DEBUG( 10,"tried to BYE non existing session in $self->{callid}" );
521 0           return;
522             }
523 0           DEBUG( 10,"close sessions $self->{callid} $idfrom -> $idto success" );
524             }
525 0           return @info;
526             }
527              
528             ############################################################################
529             # expire call, e.g. inactive sessions, unused socketgroups...
530             # Args: ($self,%args)
531             # %args: see *::Base::expire
532             # Returns: @expired
533             # @expired: list of infos about expired sessions containing, see
534             # close_session
535             ############################################################################
536             sub expire {
537 0     0     my Net::SIP::NATHelper::Call $self = shift;
538 0           my %args = @_;
539              
540 0           my $expire_unused = $args{time} - $args{unused};
541 0           my $expire_active = $args{time} - $args{active};
542              
543 0           my @expired;
544             my %active_pairs; # mapping [idfrom,idto]|[idto,idfrom] -> session.created
545 0           my $need_next_pass;
546 0           my $by_from = $self->{from};
547              
548 0           for my $pass (1,2) {
549 0           while ( my ($idfrom,$by_cseq) = each %$by_from ) {
550              
551             # start with highest cseq so that we hopefully need 2 passes
552             # for expire session which got replaced by new ones
553 0           my @cseq = sort { $b <=> $a } keys %$by_cseq;
  0            
554 0           foreach my $cseq ( @cseq ) {
555 0           my $data = $by_cseq->{$cseq};
556              
557             # drop inactive sessions
558 0           my $sessions = $data->{sessions};
559 0           foreach my $idto ( keys %$sessions ) {
560 0           my $sess = $sessions->{$idto};
561 0           my $lastmod = max($sess->lastmod,$sess->{created});
562 0 0 0       if ( $lastmod < $expire_active ) {
    0 0        
563 0           DEBUG( 10,"$self->{callid} expired session {$sess->{id}} $cseq|$idfrom|$idto because lastmod($lastmod) < active($expire_active)" );
564 0           my $sess = delete $sessions->{$idto};
565 0           push @expired, $sess->info_as_hash( $self->{callid}, $cseq, reason => 'expired' );
566              
567             } elsif ( my $created = max(
568             $active_pairs{ "$idfrom\0$idto" } || 0,
569             $active_pairs{ "$idto\0$idfrom" } || 0
570             ) ) {
571 0 0         if ( $created > $sess->{created} ) {
    0          
572 0           DEBUG( 10,"$self->{callid} removed session {$sess->{id}} $cseq|$idfrom|$idto because there is newer session" );
573 0           my $sess = delete $sessions->{$idto};
574 0           push @expired, $sess->info_as_hash( $self->{callid}, $cseq, reason => 'replaced' );
575             } elsif ( $created < $sess->{created} ) {
576             # probably a session in the other direction has started
577 0           DEBUG( 100,"there is another session with created=$created which should be removed in next pass" );
578 0           $active_pairs{ "$idfrom\0$idto" } = $sess->{created};
579 0           $need_next_pass = 1
580             }
581             } else {
582             # keep session
583 0           DEBUG( 100,"$self->{callid} session {$sess->{id}} $idfrom -> $idto created=$sess->{created} stays active in pass#$pass" );
584 0           $active_pairs{ "$idfrom\0$idto" } = $sess->{created};
585             }
586             }
587              
588             # delete socketgroups, which are not used in sessions and which
589             # are expired
590             # use string representation as key for comparison
591 0           my %used;
592 0           foreach ( values %$sessions ) {
593 0           $used{ $_->{sfrom} }++;
594 0           $used{ $_->{sto} }++;
595             }
596              
597              
598 0           my $groups = $data->{socket_groups_to};
599 0           my %expired_sg;
600 0           my @v = values(%$groups);
601 0 0         push @v,$data->{socket_group_from} if $data->{socket_group_from};
602 0           foreach my $v ( @v ) {
603 0 0         next if $used{ $v }; # used in not expired session
604 0           my $lastmod = $v->{lastmod};
605 0 0         if ( ! $lastmod ) {
    0          
606             # was never used
607 0 0         if ( $v->{created} < $expire_unused ) {
608 0           DEBUG( 10,"$self->{callid} expired socketgroup $v->{id} because created($v->{created}) < unused($expire_unused)" );
609 0           $expired_sg{$v} = 1;
610             }
611             } elsif ( $lastmod < $expire_active ) {
612 0           DEBUG( 10,"$self->{callid} expired socketgroup $v->{id} because lastmod($lastmod) < active($expire_active)" );
613 0           $expired_sg{$v} = 1;
614             }
615             }
616              
617             $data->{socket_group_from} = undef if %expired_sg
618 0 0 0       and delete( $expired_sg{ $data->{socket_group_from} } );
619 0 0         if ( %expired_sg ) {
620 0           foreach my $id (keys(%$groups)) {
621 0 0         delete $groups->{$id} if delete $expired_sg{$groups->{$id}};
622 0 0         %expired_sg || last;
623             }
624             }
625             }
626             }
627              
628             # only run again if needed
629 0 0         $need_next_pass || last;
630 0           $need_next_pass = 0;
631 0           DEBUG( 100,'need another pass' );
632             }
633 0           return @expired;
634             }
635              
636             ############################################################################
637             # check if empty, e.g. no more socket groups on the call
638             # Args: $self
639             # Returns: TRUE if empty
640             ############################################################################
641             sub is_empty {
642 0     0     my Net::SIP::NATHelper::Call $self = shift;
643 0           my $by_from = $self->{from};
644 0           foreach my $idfrom ( keys %$by_from ) {
645 0           my $by_cseq = $by_from->{$idfrom};
646 0           foreach my $cseq ( keys %$by_cseq ) {
647 0           my $data = $by_cseq->{$cseq};
648 0 0 0       if ( ! %{ $data->{socket_groups_to}} && ! $data->{socket_group_from} ) {
  0            
649 0           DEBUG( 100,"deleted unused cseq $cseq in $self->{callid}|$idfrom" );
650 0           delete $by_cseq->{$cseq};
651             }
652             }
653 0 0         if ( ! %$by_cseq ) {
654 0           DEBUG( 100,"deleted unused idfrom $idfrom in $self->{callid}" );
655 0           delete $by_from->{$idfrom};
656             }
657             }
658 0 0         return %$by_from ? 0:1;
659             }
660              
661             ############################################################################
662             # collect the callbacks for all sessions within the call
663             # Args: $self
664             # Returns: @callbacks, see Net::SIP::NATHelper::Session::callbacks
665             ############################################################################
666             sub callbacks {
667 0     0     my Net::SIP::NATHelper::Call $self = shift;
668 0           my @cb;
669 0           my $by_from = $self->{from};
670 0           foreach my $by_cseq ( values %$by_from ) {
671 0           foreach my $data ( values %$by_cseq ) {
672 0           push @cb, map { $_->callbacks } values %{ $data->{sessions} };
  0            
  0            
673             }
674             }
675 0           return @cb;
676             }
677              
678             ############################################################################
679             # run over all session and execte callback
680             # Args: $self,$callback
681             # Returns: @rv
682             # @rv: results of all callback invocations together
683             ############################################################################
684             sub sessions {
685 0     0     my Net::SIP::NATHelper::Call $self = shift;
686 0           my $callback = shift;
687 0           my $by_from = $self->{from};
688 0           my @rv;
689 0           foreach my $by_cseq ( values %$by_from ) {
690 0           foreach my $data ( values %$by_cseq ) {
691 0           push @rv, map { invoke_callback($callback,$_) }
692 0           values %{ $data->{sessions} };
  0            
693             }
694             }
695 0           return @rv;
696             }
697              
698             ############################################################################
699             # Dump debug information into string
700             # Args: $self
701             # Returns: $string
702             ############################################################################
703             sub dump {
704 0     0     my Net::SIP::NATHelper::Call $self = shift;
705 0           my $result = "-- DUMP of call $self->{callid} --\n";
706 0           my $by_from = $self->{from};
707 0           foreach my $idfrom ( sort keys %$by_from ) {
708 0           my $by_cseq = $by_from->{$idfrom};
709 0           foreach ( sort { $a <=> $b } keys %$by_cseq ) {
  0            
710 0           $result.= "-- Socket groups in $idfrom|$_ --\n";
711 0           my $sgroups = $by_cseq->{$_}{socket_groups_to};
712 0           my $sf = $by_cseq->{$_}{socket_group_from};
713 0 0         $result .= $sf->dump if $sf;
714 0           foreach ( sort keys %$sgroups ) {
715 0           $result.= $sgroups->{$_}->dump;
716             }
717 0           $result.= "-- Sessions in $idfrom|$_ --\n";
718 0           my $sessions = $by_cseq->{$_}{sessions};
719 0           foreach ( sort keys %$sessions ) {
720 0           $result.= $sessions->{$_}->dump;
721             }
722             }
723             }
724 0           return $result;
725             }
726              
727              
728             ############################################################################
729             ############################################################################
730             #
731             # Net::SIP::NATHelper::Session
732             # each session consists of two Net::SIP::NATHelper::SocketGroup's and the data
733             # are transferred between these groups
734             #
735             ############################################################################
736             ############################################################################
737              
738             package Net::SIP::NATHelper::Session;
739 4     4   29 use fields qw( sfrom sto created bytes_from bytes_to callbacks id param );
  4         8  
  4         16  
740 4     4   382 use Net::SIP::Debug;
  4         14  
  4         16  
741 4     4   29 use List::Util 'max';
  4         7  
  4         197  
742 4     4   22 use Net::SIP::Util ':all';
  4         7  
  4         698  
743 4     4   26 use Time::HiRes 'gettimeofday';
  4         8  
  4         16  
744              
745             # increased for each new session
746             my $session_id = 0;
747              
748             ############################################################################
749             # create new Session between two SocketGroup's
750             # Args: ($class,$socketgroup_from,$socketgroup_to;$param)
751             # Returns: $self
752             ############################################################################
753             sub new {
754 0     0     my ($class,$sfrom,$sto,$param) = @_;
755 0           my $self = fields::new( $class );
756              
757             # sanity check that both use the same number of sockets
758 0 0         if ( @{ $sfrom->get_socks } != @{ $sto->get_socks } ) {
  0            
  0            
759 0           DEBUG( 1,"different number of sockets in request and response" );
760 0           return;
761             }
762              
763 0           %$self = (
764             sfrom => $sfrom,
765             sto => $sto,
766             created => scalar( gettimeofday() ),
767             bytes_from => 0,
768             bytes_to => 0,
769             callbacks => undef,
770             param => $param,
771             id => ++$session_id,
772             );
773 0           return $self;
774             }
775              
776             ############################################################################
777             # returns session info as hash
778             # Args: ($self,$callid,$cseq,%more)
779             # %more: hash with more key,values to put into info
780             # Returns: %session_info
781             # %session_info: hash with callid,cseq,idfrom,idto,from,to,
782             # bytes_from,bytes_to,sessionid and %more
783             ############################################################################
784             sub info_as_hash {
785 0     0     my Net::SIP::NATHelper::Session $self = shift;
786 0           my ($callid,$cseq,%more) = @_;
787              
788             my $from = join( ",", map {
789 0           "$_->{addr}:$_->{port}/$_->{range}"
790 0           } @{ $self->{sfrom}{orig_media} } );
  0            
791              
792             my $to = join( ",", map {
793 0           "$_->{addr}:$_->{port}/$_->{range}"
794 0           } @{ $self->{sto}{orig_media} } );
  0            
795              
796             return {
797             callid => $callid,
798             cseq => $cseq,
799             idfrom => $self->{sfrom}{id},
800             idto => $self->{sto}{id},
801             from => $from,
802             to => $to,
803             bytes_from => $self->{bytes_from},
804             bytes_to => $self->{bytes_to},
805             created => $self->{created},
806             sessionid => $self->{id},
807             param => $self->{param},
808 0           %more,
809             }
810             }
811              
812             ############################################################################
813             # return time of last modification, e.g. maximum of lastmod of both
814             # socketgroups
815             # Args: $self
816             # Returns: $lastmod
817             ############################################################################
818             sub lastmod {
819 0     0     my Net::SIP::NATHelper::Session $self = shift;
820 0           return max( $self->{sfrom}{lastmod}, $self->{sto}{lastmod} );
821             }
822              
823             ############################################################################
824             # return all [ socket, callback,cbid ] tuples for the session
825             # cbid is uniq for each callback and can be used to detect, which callbacks
826             # changed compared to the last call
827             # Args: $self
828             # Returns: @callbacks
829             ############################################################################
830              
831             my $callback_id = 0; # uniq id for each callback
832             sub callbacks {
833 0     0     my Net::SIP::NATHelper::Session $self = shift;
834              
835 0           my $callbacks = $self->{callbacks};
836 0 0         return @$callbacks if $callbacks; # already computed
837              
838             # data received on sockets in $sfrom will be forwarded to the original
839             # target from $sfrom using the matching socket from $sto and the other
840             # way around.
841             # This means we do symetric RTP in all cases
842              
843 0           my $sfrom = $self->{sfrom};
844 0           my $sockets_from = $sfrom->get_socks;
845 0           my $targets_from = $sfrom->get_targets;
846              
847 0           my $sto = $self->{sto};
848 0           my $sockets_to = $sto->get_socks;
849 0           my $targets_to = $sto->get_targets;
850              
851 0           my $fwd_data = $self->can('forward_data');
852              
853 0           my @cb;
854 0           for( my $i=0;$i<@$sockets_from;$i++ ) {
855             # If we detect, that the peer does symmetric RTP we connect the
856             # socket and set the addr to undef to make sure that we use send
857             # and not sendto when forwarding the data
858 0           my $recvaddr = $targets_to->[$i];
859 0           my $dstaddr = $targets_from->[$i];
860              
861             $dstaddr && push @cb, [
862             $sockets_from->[$i],
863             [
864             $fwd_data,
865             $sockets_from->[$i], # read data from socket FROM(nat)
866             $sockets_to->[$i], # forward them using socket TO(nat)
867             \$recvaddr,\$dstaddr, # will be set to undef once connected
868             $sfrom, # call $sfrom->didit
869             \$self->{bytes_to}, # to count bytes coming from 'to'
870             $self->{id}, # for debug messages
871 0 0         ],
872             ++$callback_id
873             ];
874              
875             $recvaddr && push @cb, [
876             $sockets_to->[$i],
877             [
878             $fwd_data,
879             $sockets_to->[$i], # read data from socket TO(nat)
880             $sockets_from->[$i], # forward data using socket FROM(nat)
881             \$dstaddr,\$recvaddr, # will be set to undef once connected
882             $sto, # call $sto->didit
883             \$self->{bytes_from}, # to count bytes coming from 'from'
884             $self->{id}, # for debug messages
885 0 0         ],
886             ++$callback_id
887             ];
888             }
889 0           $self->{callbacks} = \@cb; # cache
890 0           return @cb;
891             }
892              
893             ############################################################################
894             # function used for forwarding data in callbacks()
895             ############################################################################
896             sub forward_data {
897 0     0     my ($read_socket,$write_socket,$rfrom,$rto,$group,$bytes,$id) = @_;
898 0   0       my $peer = recv( $read_socket, my $buf,2**16,0 ) || do {
899             DEBUG( 10,"recv data failed: $!" );
900             return;
901             };
902              
903 0     0     my $name = sub { ip_sockaddr2string(shift) };
  0            
904              
905 0 0 0       if ( ! $$bytes ) {
    0          
906 0 0         if ( $peer eq $$rfrom ) {
907 0           DEBUG( 10,"peer ".$name->($peer).
908             " uses symmetric RTP, connecting sockets");
909 0 0         $$rfrom = undef if connect($read_socket,$peer);
910             } else {
911             # set rfrom to peer for later checks
912 0           $$rfrom = $peer;
913             }
914             } elsif ( $$rfrom && $peer ne $$rfrom ) {
915             # the previous packet was from another peer, ignore this data
916 0           DEBUG( 10,"{$id} ignoring unexpected data from %s on %s, expecting data from %s instead",
917             $name->($peer), $name->(getsockname($read_socket)),$name->($$rfrom));
918             }
919              
920 0           my $l = length($buf);
921 0           $$bytes += $l;
922 0           $group->didit($l);
923              
924 0 0         if ( $$rto ) {
925 0 0         send( $write_socket, $buf,0, $$rto ) || do {
926 0           DEBUG( 10,"send data failed: $!" );
927 0           return;
928             };
929 0           DEBUG( 50,"{$id} transferred %d bytes on %s via %s to %s",
930             length($buf), $name->( getsockname($read_socket )),
931             $name->(getsockname( $write_socket )),$name->($$rto));
932             } else {
933             # using connected socket
934 0 0         send( $write_socket, $buf,0 ) || do {
935 0           DEBUG( 10,"send data failed: $!" );
936 0           return;
937             };
938 0           DEBUG( 50,"{$id} transferred %d bytes on %s via %s to %s",
939             length($buf), $name->( getsockname($read_socket )),
940             $name->(getsockname( $write_socket )),
941             $name->(getpeername( $write_socket )));
942             }
943             }
944              
945              
946             ############################################################################
947             # Dump debug information into string
948             # Args: $self
949             # Returns: $string
950             ############################################################################
951             sub dump {
952 0     0     my Net::SIP::NATHelper::Session $self = shift;
953             return "{$self->{id}}".
954             ( $self->{sfrom} && $self->{sfrom}{id} || 'NO.SFROM' ).",".
955 0   0       ( $self->{sto} && $self->{sto}{id} || 'NO.STO' )."\n";
      0        
956             }
957              
958             ############################################################################
959             ############################################################################
960             #
961             # Net::SIP::NATHelper::SocketGroup
962             # manages groups of sockets created from an SDP body
963             # manages the local (NAT) sockets and the original targets from the SDP
964             #
965             ############################################################################
966             ############################################################################
967              
968             package Net::SIP::NATHelper::SocketGroup;
969 4     4   3859 use fields qw( id created lastmod new_media orig_media nathelper );
  4         7  
  4         16  
970 4     4   532 use Net::SIP::Debug;
  4         7  
  4         22  
971 4     4   25 use Time::HiRes 'gettimeofday';
  4         8  
  4         16  
972 4     4   292 use Socket;
  4         8  
  4         3257  
973              
974             ############################################################################
975             # create new socket group based on the original media and a local address
976             # Args: ($class,$nathelper,$id,$new_addr,$media)
977             # Returns: $self|()
978             # Comment: () will be returned if allocation of sockets fails
979             ############################################################################
980             sub new {
981 0     0     my ($class,$nathelper,$id,$new_addr,$media) = @_;
982 0 0         my $new_media = $nathelper->get_rtp_sockets( $new_addr,$media )
983             or return;
984              
985 0           my $self = fields::new($class);
986 0           %$self = (
987             nathelper => $nathelper,
988             id => $id,
989             orig_media => [ @$media ],
990             new_media => $new_media,
991             lastmod => 0,
992             created => scalar( gettimeofday() ),
993             );
994 0           return $self;
995             }
996              
997             ############################################################################
998             # give allocated sockets back to NATHelper
999             ############################################################################
1000             sub DESTROY {
1001 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1002             ($self->{nathelper} || return )->unget_rtp_sockets( $self->{new_media} )
1003 0   0       }
1004              
1005              
1006             ############################################################################
1007             # updates timestamp of last modification, used in expiring
1008             # Args: ($self)
1009             # Returns: NONE
1010             ############################################################################
1011             sub didit {
1012 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1013 0           $self->{lastmod} = gettimeofday();
1014             }
1015              
1016             ############################################################################
1017             # returns \@list of media [ip,port,range] in group
1018             # Args: $self
1019             # Returns: \@media
1020             ############################################################################
1021             sub get_media {
1022 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1023             my @media = map { [
1024             $_->[0], # addr
1025             $_->[1], # base port
1026 0           int(@{$_->[2]}) # range, e.g number of sockets
  0            
1027 0           ]} @{ $self->{new_media} };
  0            
1028 0           return \@media;
1029             }
1030              
1031             ############################################################################
1032             # returns \@list of sockets in group
1033             # Args: $self
1034             # Returns: \@sockets
1035             ############################################################################
1036             sub get_socks {
1037 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1038 0           return [ map { @{$_->[2]} } @{$self->{new_media}} ];
  0            
  0            
  0            
1039             }
1040              
1041             ############################################################################
1042             # returns \@list of the original targets in group
1043             # Args: $self
1044             # Returns: \@targets
1045             ############################################################################
1046             sub get_targets {
1047 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1048 0           return [ map { @{$_->[3]} } @{$self->{new_media}} ];
  0            
  0            
  0            
1049             }
1050              
1051             ############################################################################
1052             # Dump debug information into string
1053             # Args: $self
1054             # Returns: $string
1055             ############################################################################
1056             sub dump {
1057 0     0     my Net::SIP::NATHelper::SocketGroup $self = shift;
1058             my $result = $self->{id}." >> ".join( ' ',
1059 0           map { "$_->[0]:$_->[1]/$_->[2]" }
1060 0           @{$self->get_media} ).
  0            
1061             "\n";
1062 0           return $result;
1063             }
1064              
1065             1;