File Coverage

blib/lib/Net/SIP/NATHelper/Base.pm
Criterion Covered Total %
statement 69 455 15.1
branch 0 118 0.0
condition 0 71 0.0
subroutine 23 60 38.3
pod 11 12 91.6
total 103 716 14.3


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