File Coverage

blib/lib/Netflow/Parser.pm
Criterion Covered Total %
statement 39 159 24.5
branch 0 50 0.0
condition 0 6 0.0
subroutine 13 28 46.4
pod 4 4 100.0
total 56 247 22.6


line stmt bran cond sub pod time code
1             package Netflow::Parser;
2              
3 1     1   15030 use 5.006;
  1         3  
  1         38  
4 1     1   7 use strict;
  1         2  
  1         35  
5 1     1   5 use warnings;
  1         5  
  1         41  
6              
7 1         3 use fields qw/
8             templates
9             flow_cb
10             verbose
11 1     1   437 /;
  1         1090  
12 1     1   57 use Carp;
  1         1  
  1         1030  
13              
14             =head1 NAME
15              
16             Netflow::Parser
17              
18             =head1 DESCRIPTION
19              
20             Netflow Parser supports currently only Netflow V9.
21              
22             =head1 VERSION
23              
24             Version 0.01
25              
26             =cut
27              
28             $Netflow::Parser::VERSION = '0.02';
29              
30             =head1 SYNOPSIS
31              
32             use Netflow::Parser;
33              
34             my $nfp = Netflow::Parser->new(
35             flow_cb => sub {my ($flow_hr) = @_; ...},
36             templates_data => pack('H*', '01020002011b000400e60001')
37             );
38              
39             while(my $packet = take_packet_from_socket()) {
40             my $pp = $nfp->parse($packet);
41              
42             # version, count, sysuptime, unix_secs, seqno and source_id
43             $pp->header;
44              
45             # parsed flowsets
46             $pp->parsed;
47              
48             # unparsed flowsets
49             $pp->unparsed && persist_for_later($pp->unparsed);
50             }
51              
52             # persist templates if you want
53             my @templates = $nfp->templates;
54             foreach (@templates) {
55             my ($id, $content) = each(%{$_});
56             }
57              
58             =head1 SUBROUTINES/METHODS
59              
60             =head2 new(%opts)
61              
62             options:
63              
64             =over
65              
66             =item
67              
68             C
69              
70             [raw template piece]
71              
72             =item
73              
74             C
75              
76             callback method will be applied to each parsed flow
77              
78             =item
79              
80             C
81              
82             =back
83              
84             =cut
85              
86             sub new {
87 0     0 1   my Netflow::Parser $self = shift;
88 0           my (%opts) = @_;
89 0 0         unless (ref $self) {
90 0           $self = fields::new($self);
91             }
92              
93 0           $self->{'verbose'} = delete $opts{'verbose'};
94 0 0         if ($opts{'flow_cb'}) {
95 0           $self->{'flow_cb'} = delete $opts{'flow_cb'};
96             }
97              
98 0           my $templates = delete $opts{'templates_data'};
99 0           foreach (@{$templates}) {
  0            
100 0           $self->_parse_template_v9($_);
101             }
102              
103             %opts
104 0 0         && warn(sprintf "unsupported parameter(s) '%s'", join ', ', keys %opts);
105              
106 0           return $self;
107             } ## end sub new
108              
109             =head2 parse($packet)
110              
111             currently only NetFlow V9 supported
112              
113             unpack packet, try to parse flowsets content.
114              
115             return {
116             'header' => {
117             'count',
118             'seqno',
119             'source_id',
120             'sysuptime',
121             'unix_secs',
122             'version' => 9
123             },
124             'flows' => [flow_cb result],
125             'flowsets' => ?, # flowsets number
126             'templates' => [], # templates contains in the packet
127             'unparsed_flowsets' => [] # no template
128             }
129              
130              
131             =cut
132              
133             sub parse {
134 0     0 1   my ($self, $packet) = @_;
135              
136             #my ($version) = unpack("n", $packet);
137 0           return $self->_parse_v9($packet);
138             } ## end sub parse
139              
140             =head2 templates()
141              
142             return [ { template_id => content} ]
143              
144             =cut
145              
146             sub templates {
147 0     0 1   my ($self) = @_;
148 0           my @templates = ();
149 0           foreach my $id (keys %{ $self->{'templates'} }) {
  0            
150 0           push @templates, { $id => $self->template($id) };
151             }
152 0           return @templates;
153             } ## end sub templates
154              
155             =head2 template($template_id)
156              
157             return hex dump of template for given $template_id
158              
159             =cut
160              
161             sub template {
162 0     0 1   my ($self, $template_id) = @_;
163 0 0         unless ($self->{'templates'}->{$template_id}) {
164 0 0         $self->{'verbose'} && $self->_debug("no template $template_id");
165 0           return;
166             }
167              
168 0           return pack('n*',
169             $template_id,
170 0           scalar(@{ $self->{'templates'}->{$template_id}->{'content'} }) / 2,
171 0           @{ $self->{'templates'}->{$template_id}->{'content'} });
172             } ## end sub template
173              
174             #=head2 _parse_v9($packet)
175             #
176             #parse a C<$packet> and return content of them
177             #
178             #return {
179             # 'header' => {
180             # 'version' => $version,
181             # 'count' => $count,
182             # 'sysuptime' => $sysuptime,
183             # 'unix_secs' => $unix_secs,
184             # 'seqno' => $seqno,
185             # 'source_id' => $source_id,
186             # },
187             # 'templates' => [parsed templates],
188             # 'flows' => [parsed flows],
189             # 'unparsed_flowsets' => [flowset couldn't be parsed],
190             # 'flowsets' => scalar(@flowsets),
191             # }
192             #
193             #=cut
194              
195             sub _parse_v9 {
196 0     0     my ($self, $packet) = @_;
197             my (
198 0           $version, $count, $sysuptime, $unix_secs,
199             $seqno, $source_id, @flowsets
200             ) = unpack("nnNNNN(nnX4/a)*", $packet);
201              
202 0 0         eval { $version == 9 } || Carp::croak("the version of packet is not v9");
  0            
203              
204 0           my $pp = Netflow::Parser::Packet->new(
205             'flowsets' => scalar(@flowsets),
206             'header' => {
207             'version' => $version,
208             'count' => $count,
209             'sysuptime' => $sysuptime,
210             'unix_secs' => $unix_secs,
211             'seqno' => $seqno,
212             'source_id' => $source_id,
213             }
214             );
215              
216 0 0         scalar(@flowsets) > length($packet)
217             && warn sprintf("extimated %d flowsets > paket length %d",
218             scalar(@flowsets), length($packet));
219 0           for (my $i = 0; $i < scalar(@flowsets); $i += 2) {
220 0           my $flowset_id = $flowsets[$i];
221              
222             # chop off id/length
223 0           my $flowset = substr($flowsets[$i + 1], 4);
224 0 0         if ($flowset_id == 0) {
    0          
    0          
225 0 0         if ($flowset) {
226 0           my @tmpl = $self->parse_template_v9($flowset);
227 0 0         if (@tmpl) {
228 0           $pp->add_template(@tmpl);
229             }
230             else {
231 0           $pp->add_unparsed({ $flowset_id => $flowset });
232             }
233             } ## end if ($flowset)
234             } ## end if ($flowset_id == 0)
235             elsif ($flowset_id == 1) {
236              
237             # 1 - Options Template FlowSet
238 0 0         $self->{'verbose'} && $self->_debug("do nothing for flowset id 1");
239              
240 0           $pp->add_unparsed({ $flowset_id => $flowset });
241             } ## end elsif ($flowset_id == 1)
242             elsif ($flowset_id > 255) {
243 0           my @flows = $self->_parse_flowset_v9($flowset_id, $flowset);
244 0 0         if (scalar(@flows)) {
245 0           $pp->add_parsed({ $flowset_id => [@flows] });
246             }
247             else {
248 0           $pp->add_unparsed({ $flowset_id => $flowset });
249             }
250             } ## end elsif ($flowset_id > 255)
251             else {
252             # reserved FlowSet
253 0 0         $self->{'verbose'}
254             && $self->_debug("Unknown FlowSet ID $flowset_id found");
255             }
256             } ## end for (my $i = 0; $i < scalar...)
257              
258 0           return $pp;
259             } ## end sub _parse_v9
260              
261             #=head2 _parse_flowset_v9 ($flowset_id, $flowset)
262             #
263             #parse flowset if defined instance template for $flowset_id
264             #
265             #apply C to each flow
266             #
267             #=over
268             #
269             #=item C<$flowset_id>
270             #
271             #is a template id number
272             #
273             #=item C<$flowset>
274             #
275             #flowset data
276             #
277             #=back
278             #
279             #return [{flow}]
280             #
281             #=cut
282              
283             sub _parse_flowset_v9 {
284 0     0     my ($self, $flowset_id, $flowset) = @_;
285 0 0         if (!defined($self->{'templates'}->{$flowset_id})) {
286 0 0         $self->{'verbose'}
287             && $self->_debug("unknown template id $flowset_id");
288 0           return;
289             }
290              
291 0           my ($tmpl_length, @template) = (
292             $self->{'templates'}->{$flowset_id}->{'length'},
293 0           @{ $self->{'templates'}->{$flowset_id}->{'content'} }
294             );
295              
296 0           my $cb = $self->{flow_cb};
297 0           my ($datalen, $ofs) = (length($flowset), 0);
298              
299 0           my @flows = ();
300 0           while (($ofs + $tmpl_length) <= $datalen) {
301 0           my $flow = {};
302 0           for (my $i = 0; $i < scalar @template; $i += 2) {
303 0           my $fld_type = $template[$i];
304 0           my $fld_len = $template[$i + 1];
305 0           my $fld_val = substr($flowset, $ofs, $fld_len);
306 0           $ofs += $fld_len;
307              
308 0           $flow->{$fld_type} = $fld_val;
309             } ## end for (my $i = 0; $i < scalar...)
310              
311 0 0         $cb && $cb->($flow);
312              
313 0           push @flows, $flow;
314             } ## end while (($ofs + $tmpl_length...))
315              
316 0           return @flows;
317             } ## end sub _parse_flowset_v9
318              
319             #=head2 _parse_template_v9($flowset)
320             #
321             #parse $flowset data, update instance templates
322             #
323             #return ({template_id => [template content]})
324             #
325             #=cut
326              
327             sub _parse_template_v9 {
328 0     0     my ($self, $flowset) = @_;
329 0           my @template_ints = unpack("n*", $flowset);
330 0           my ($i, $count, @tmpl) = (0, scalar(@template_ints));
331 0           while ($i < $count) {
332 0           my $template_id = $template_ints[$i];
333 0           my $fld_count = $template_ints[$i + 1];
334 0 0 0       last if (!defined($template_id) || !defined($fld_count));
335              
336             #TODO $template_id < 255 || $template_id > 300; is 300 enough?
337 0 0 0       ($template_id < 255 || $template_id > 300)
338             && Carp::croak("wrong template id: $template_id");
339              
340 0           my $content
341             = [@template_ints[($i + 2) .. ($i + 2 + $fld_count * 2 - 1)]];
342 0           my $totallen = 0;
343 0           for (my $j = 1; $j < scalar @$content; $j += 2) {
344 0           $totallen += $content->[$j];
345             }
346              
347 0 0         $self->{'verbose'}
    0          
348             && $self->_debug(
349             $self->{'templates'}->{$template_id}
350             ? "update templates item $template_id"
351             : "add $template_id to templates",
352             "content: $totallen",
353             "length: $totallen"
354             );
355              
356 0           $self->{'templates'}->{$template_id} = {
357             'content' => $content,
358             'length' => $totallen
359             };
360              
361 0           $i += (2 + $fld_count * 2);
362              
363 0           push @tmpl, { $template_id => $content };
364             } ## end while ($i < $count)
365              
366 0           return @tmpl;
367             } ## end sub _parse_template_v9
368              
369             sub _debug {
370 0     0     my ($self, @msg) = @_;
371 0           (undef, undef, my $line) = caller;
372 0           print join(' ', "LINE[$line]:", @msg, $/);
373             }
374              
375             =head1 AUTHOR
376              
377             Alexei Pastuchov Epalik at cpan.orgE.
378              
379             =head1 REPOSITORY
380              
381             L
382              
383             =head1 LICENSE AND COPYRIGHT
384              
385             Copyright 2014 by Alexei Pastuchov Epalik at cpan.orgE.
386              
387             This library is free software; you can redistribute it and/or modify
388             it under the same terms as Perl itself.
389              
390             =cut
391              
392             1; # End of Netflow::Parser
393              
394             {
395              
396             package Netflow::Parser::Packet;
397 1     1   7 use strict;
  1         2  
  1         44  
398 1     1   6 use warnings;
  1         2  
  1         37  
399              
400 1         3 use fields qw/
401             flowsets
402             header
403             templates
404             parsed
405             unparsed
406 1     1   9 /;
  1         1  
407              
408             {
409 1     1   65 no strict 'refs';
  1         1  
  1         236  
410             foreach my $k (keys %{'Netflow::Parser::Packet::FIELDS'}) {
411 0     0     *{$k} = sub { shift->{$k} }
412             }
413             };
414              
415             sub new {
416 0     0     my Netflow::Parser::Packet $self = shift;
417 0           my (%opts) = @_;
418 0 0         unless (ref $self) {
419 0           $self = fields::new($self);
420             }
421              
422 0           $self->{header}
423 0           = Netflow::Parser::Packet::Header->new(%{ delete $opts{header} });
424              
425 0           $self->{flowsets} = delete $opts{flowsets};
426              
427 0           foreach (qw/templates parsed unparsed/) {
428 0           $self->{$_} = [];
429             }
430              
431 0           return $self;
432             } ## end sub new
433              
434             sub add_template {
435 0     0     my ($self, $tmpl) = @_;
436 0           push @{ $self->{template} }, $tmpl;
  0            
437             }
438              
439             sub add_parsed {
440 0     0     my ($self, $hr) = @_;
441 0           push @{ $self->{parsed} }, $hr;
  0            
442             }
443              
444             sub add_unparsed {
445 0     0     my ($self, $hr) = @_;
446 0           push @{ $self->{unparsed} }, $hr;
  0            
447             }
448              
449             }
450              
451             1; # End of Netflow::Parser::Packet
452              
453             {
454              
455             package Netflow::Parser::Packet::Header;
456 1     1   5 use strict;
  1         1  
  1         26  
457 1     1   4 use warnings;
  1         1  
  1         27  
458              
459 1         7 use fields qw/
460             version
461             count
462             sysuptime
463             unix_secs
464             seqno
465             source_id
466 1     1   4 /;
  1         0  
467              
468             my @fields;
469             {
470 1     1   67 no strict 'refs';
  1         0  
  1         140  
471             @fields = keys %{'Netflow::Parser::Packet::Header::FIELDS'};
472             foreach (@fields) {
473 0     0     *{$_} = sub { shift->{$_} }
474             }
475             };
476              
477             sub new {
478 0     0     my Netflow::Parser::Packet::Header $self = shift;
479 0           my (%opts) = @_;
480 0 0         unless (ref $self) {
481 0           $self = fields::new($self);
482             }
483              
484 0           foreach (@fields) {
485 0 0         $opts{$_} || next;
486 0           $self->{$_} = delete $opts{$_};
487             }
488              
489 0           return $self;
490             } ## end sub new
491             }
492              
493             1; # End of Netflow::Parser::Packet::Header