#!/usr/bin/perl use strict; use warnings; use Linux::Inotify2; use AnyEvent::Loop; use AnyEvent; use Fcntl; use Errno qw(EINTR EIO EAGAIN EWOULDBLOCK :POSIX); use Net::Pcap; use Time::HiRes; my $COMBINE = 1; # possible values: # 0: don't combine any streams. each stream gets written to its own pcap file # 1: combine all streams of one call into one pcap file my $i = new Linux::Inotify2 or die; $i->blocking(0); $i->watch('/var/spool/rtpengine', IN_CLOSE_WRITE | IN_DELETE, \&handle_inotify) or die; my $i_w = AnyEvent->io(fh => $i->fileno, poll => 'r', cb => sub { $i->poll }); setup(); AnyEvent::Loop::run(); exit; my %metafiles; my %callbacks; sub handle_inotify { my ($e) = @_; my $fn = $e->{w}->{name} . '/' . $e->{name}; my $mf = ($metafiles{$fn} //= { name => $fn }); if ($e->IN_DELETE) { handle_delete($e, $fn, $mf); } elsif ($e->IN_CLOSE_WRITE) { handle_change($e, $fn, $mf); } else { print("unhandled inotify event on $fn\n"); } } sub handle_change { my ($e, $fn, $mf) = @_; print("handling change on $fn\n"); my $fd; open($fd, '<', $fn) or return; # resume from where we left of my $pos = $mf->{pos} // 0; seek($fd, $pos, 0); # read as much as we can my $buf; read($fd, $buf, 100000) or return; $mf->{pos} = tell($fd); close($fd); # read contents section by section while ($buf =~ s/^(.*?)\n//s) { my $key = $1; $buf =~ s/^(\d+):\n//s or die $buf; my $len = $1; my $val = substr($buf, 0, $len, ''); $buf =~ s/^\n\n//s or die; if ($key =~ /^(CALL-ID|PARENT)$/) { $mf->{$key} = $val; } elsif ($key =~ /^STREAM (\d+) interface$/) { open_stream($mf, $val, $1); } elsif ($key =~ /^STREAM (\d+) details$/) { stream_details($mf, $val, $1); } } cb('call_setup', $mf); } sub handle_delete { my ($e, $fn, $mf) = @_; print("handling delete on $fn\n"); cb('call_close', $mf); for my $sn (keys(%{$mf->{streams}})) { my $ref = $mf->{streams}->{$sn}; close_stream($ref); } delete($mf->{streams}); delete($mf->{streams_id}); delete($metafiles{$fn}); } sub get_stream_by_id { my ($mf, $id) = @_; my $ref = ($mf->{streams_id}->[$id] //= { metafile => $mf, id => $id }); return $ref; } sub open_stream { my ($mf, $stream, $id) = @_; print("opening $stream for $mf->{'CALL-ID'}\n"); my $fd; sysopen($fd, '/proc/rtpengine/0/calls/' . $mf->{PARENT} . '/' . $stream, O_RDONLY | O_NONBLOCK) or return; my $ref = get_stream_by_id($mf, $id); $ref->{name} = $stream; $ref->{fh} = $fd; $ref->{watcher} = AnyEvent->io(fh => $fd, poll => 'r', cb => sub { stream_read($mf, $ref) }); cb('stream_setup', $ref, $mf); $mf->{streams}->{$stream} = $ref; $mf->{streams_id}->[$id] = $ref; print("opened for reading $stream for $mf->{'CALL-ID'}\n"); } sub stream_details { my ($mf, $val, $id) = @_; my $ref = get_stream_by_id($mf, $id); my @details = $val =~ /(\w+) (\d+)/g; while (@details) { my $k = shift(@details); my $v = shift(@details); $ref->{$k} = $v; } } sub close_stream { my ($ref) = @_; # this needs to be done explicitly, otherwise the closure would keep # the object from being freed delete($ref->{watcher}); my $mf = $ref->{metafile}; delete($mf->{streams}->{$ref->{name}}); cb('stream_close', $ref); print("closed $ref->{name}\n"); } sub stream_read { my ($mf, $ref) = @_; #print("handling read event for $mf->{name} / $ref->{name}\n"); while (1) { my $buf; my $ret = sysread($ref->{fh}, $buf, 65535); if (!defined($ret)) { if ($!{EAGAIN} || $!{EWOULDBLOCK}) { return; } print("read error on $ref->{name} for $mf->{'CALL-ID'}: $!\n"); # fall through } elsif ($ret == 0) { print("eof on $ref->{name} for $mf->{'CALL-ID'}\n"); # fall through } else { # $ret > 0 #print("$ret bytes read from $ref->{name} for $mf->{'CALL-ID'}\n"); cb('packet', $ref, $mf, $buf, $ret); next; } # some kind of error close_stream($ref); return; } } sub tvsec_now { my ($h) = @_; my @now = Time::HiRes::gettimeofday(); $h->{tv_sec} = $now[0]; $h->{tv_usec} = $now[1]; } sub setup { if ($COMBINE == 0) { $callbacks{stream_setup} = \&stream_pcap; $callbacks{stream_close} = \&stream_pcap_close; $callbacks{packet} = \&stream_packet; } elsif ($COMBINE == 1) { $callbacks{call_setup} = \&call_pcap; $callbacks{call_close} = \&call_pcap_close; $callbacks{packet} = \&call_packet; } } sub cb { my ($name, @args) = @_; my $fn = $callbacks{$name}; $fn or return; return $fn->(@args); } sub dump_open { my ($hash, $name) = @_; $hash->{pcap} = pcap_open_dead(DLT_RAW, 65535); $hash->{dumper} = pcap_dump_open($hash->{pcap}, $name); } sub dump_close { my ($hash) = @_; pcap_dump_close($hash->{dumper}); pcap_close($hash->{pcap}); delete($hash->{dumper}); delete($hash->{pcap}); } sub dump_packet { my ($hash, $buf, $len) = @_; if (!$hash->{dumper}) { print("discarding packet (dumper not open) - $hash->{name}\n"); return; } my $hdr = { len => $len, caplen => $len }; tvsec_now($hdr); pcap_dump($hash->{dumper}, $hdr, $buf); } # COMBINE 0 functions sub stream_pcap { my ($ref, $mf) = @_; dump_open($ref, $mf->{PARENT} . '-' . $ref->{name} . '.pcap'); } sub stream_pcap_close { my ($ref) = @_; dump_close($ref); } sub stream_packet { my ($ref, $mf, $buf, $ret) = @_; dump_packet($ref, $buf, $ret); } # COMBINE 1 functions sub call_pcap { my ($mf) = @_; $mf->{pcap} and return; $mf->{PARENT} or return; print("opening pcap for $mf->{PARENT}\n"); dump_open($mf, $mf->{PARENT} . '.pcap'); } sub call_pcap_close { my ($mf) = @_; dump_close($mf); } sub call_packet { my ($ref, $mf, $buf, $ret) = @_; dump_packet($mf, $buf, $ret); }