File Coverage

blib/lib/Netflow/Parser.pm
Criterion Covered Total %
statement 138 158 87.3
branch 19 50 38.0
condition 2 6 33.3
subroutine 25 28 89.2
pod 4 4 100.0
total 188 246 76.4


line stmt bran cond sub pod time code
1             package Netflow::Parser;
2              
3 2     2   236020 use 5.006;
  2         7  
4 2     2   11 use strict;
  2         5  
  2         44  
5 2     2   10 use warnings;
  2         14  
  2         66  
6              
7 2         12 use fields qw/
8             templates
9             flow_cb
10             verbose
11 2     2   1863 /;
  2         3048  
12 2     2   141 use Carp;
  2         5  
  2         2755  
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.03
25              
26             =cut
27              
28             $Netflow::Parser::VERSION = '0.03';
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 1     1 1 507 my Netflow::Parser $self = shift;
88 1         18 my (%opts) = @_;
89 1 50       5 unless (ref $self) {
90 1         4 $self = fields::new($self);
91             }
92              
93 1         14054 $self->{'verbose'} = delete $opts{'verbose'};
94 1 50       14 if ($opts{'flow_cb'}) {
95 1         8 $self->{'flow_cb'} = delete $opts{'flow_cb'};
96             }
97              
98 1         4 my $templates = delete $opts{'templates_data'};
99 1         4 foreach (@{$templates}) {
  1         7  
100 1         9 $self->_parse_template_v9($_);
101             }
102              
103             %opts
104 1 50       10 && warn(sprintf "unsupported parameter(s) '%s'", join ', ', keys %opts);
105              
106 1         10 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 1     1 1 1581 my ($self, $packet) = @_;
135              
136             #my ($version) = unpack("n", $packet);
137 1         9 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 1     1 1 7 my ($self) = @_;
148 1         9 my @templates = ();
149 1         4 foreach my $id (keys %{ $self->{'templates'} }) {
  1         12  
150 1         10 push @templates, { $id => $self->template($id) };
151             }
152 1         10 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 2     2 1 849 my ($self, $template_id) = @_;
163 2 50       16 unless ($self->{'templates'}->{$template_id}) {
164 0 0       0 $self->{'verbose'} && $self->_debug("no template $template_id");
165 0         0 return;
166             }
167              
168             return pack('n*',
169             $template_id,
170 2         15 scalar(@{ $self->{'templates'}->{$template_id}->{'content'} }) / 2,
171 2         7 @{ $self->{'templates'}->{$template_id}->{'content'} });
  2         43  
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 1     1   3 my ($self, $packet) = @_;
197             my (
198 1         15 $version, $count, $sysuptime, $unix_secs,
199             $seqno, $source_id, @flowsets
200             ) = unpack("nnNNNN(nnX4/a)*", $packet);
201              
202 1 50       5 eval { $version == 9 } || Carp::croak("the version of packet is not v9");
  1         8  
203              
204 1         19 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 1 50       11 scalar(@flowsets) > length($packet)
217             && warn sprintf("extimated %d flowsets > paket length %d",
218             scalar(@flowsets), length($packet));
219 1         18 for (my $i = 0; $i < scalar(@flowsets); $i += 2) {
220 1         5 my $flowset_id = $flowsets[$i];
221              
222             # chop off id/length
223 1         8 my $flowset = substr($flowsets[$i + 1], 4);
224 1 50       13 if ($flowset_id == 0) {
    50          
    50          
225 0 0       0 if ($flowset) {
226 0         0 my @tmpl = $self->_parse_template_v9($flowset);
227 0 0       0 if (@tmpl) {
228 0         0 $pp->add_template(@tmpl);
229             }
230             else {
231 0         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       0 $self->{'verbose'} && $self->_debug("do nothing for flowset id 1");
239              
240 0         0 $pp->add_unparsed({ $flowset_id => $flowset });
241             } ## end elsif ($flowset_id == 1)
242             elsif ($flowset_id > 255) {
243 1         7 my @flows = $self->_parse_flowset_v9($flowset_id, $flowset);
244 1 50       5 if (scalar(@flows)) {
245 1         9 $pp->add_parsed({ $flowset_id => [@flows] });
246             }
247             else {
248 0         0 $pp->add_unparsed({ $flowset_id => $flowset });
249             }
250             } ## end elsif ($flowset_id > 255)
251             else {
252             # reserved FlowSet
253 0 0       0 $self->{'verbose'}
254             && $self->_debug("Unknown FlowSet ID $flowset_id found");
255             }
256             } ## end for (my $i = 0; $i < scalar...)
257              
258 1         10 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 1     1   5 my ($self, $flowset_id, $flowset) = @_;
285 1 50       10 if (!defined($self->{'templates'}->{$flowset_id})) {
286 0 0       0 $self->{'verbose'}
287             && $self->_debug("unknown template id $flowset_id");
288 0         0 return;
289             }
290              
291             my ($tmpl_length, @template) = (
292             $self->{'templates'}->{$flowset_id}->{'length'},
293 1         4 @{ $self->{'templates'}->{$flowset_id}->{'content'} }
  1         9  
294             );
295              
296 1         6 my $cb = $self->{flow_cb};
297 1         4 my ($datalen, $ofs) = (length($flowset), 0);
298              
299 1         4 my @flows = ();
300 1         8 while (($ofs + $tmpl_length) <= $datalen) {
301 27         50 my $flow = {};
302 27         97 for (my $i = 0; $i < scalar @template; $i += 2) {
303 324         525 my $fld_type = $template[$i];
304 324         476 my $fld_len = $template[$i + 1];
305 324         562 my $fld_val = substr($flowset, $ofs, $fld_len);
306 324         427 $ofs += $fld_len;
307              
308 324         1391 $flow->{$fld_type} = $fld_val;
309             } ## end for (my $i = 0; $i < scalar...)
310              
311 27 50       116 $cb && $cb->($flow);
312              
313 27         16740 push @flows, $flow;
314             } ## end while (($ofs + $tmpl_length...))
315              
316 1         11 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 1     1   4 my ($self, $flowset) = @_;
329 1         19 my @template_ints = unpack("n*", $flowset);
330 1         7 my ($i, $count, @tmpl) = (0, scalar(@template_ints));
331 1         15 while ($i < $count) {
332 1         6 my $template_id = $template_ints[$i];
333 1         5 my $fld_count = $template_ints[$i + 1];
334 1 50 33     13 last if (!defined($template_id) || !defined($fld_count));
335              
336             #TODO $template_id < 255 || $template_id > 300; is 300 enough?
337 1 50 33     12 ($template_id < 255 || $template_id > 300)
338             && Carp::croak("wrong template id: $template_id");
339              
340 1         11 my $content
341             = [@template_ints[($i + 2) .. ($i + 2 + $fld_count * 2 - 1)]];
342 1         5 my $totallen = 0;
343 1         8 for (my $j = 1; $j < scalar @$content; $j += 2) {
344 12         49 $totallen += $content->[$j];
345             }
346              
347             $self->{'verbose'}
348             && $self->_debug(
349 1 50       20 $self->{'templates'}->{$template_id}
    50          
350             ? "update templates item $template_id"
351             : "add $template_id to templates",
352             "content: $totallen",
353             "length: $totallen"
354             );
355              
356 1         12 $self->{'templates'}->{$template_id} = {
357             'content' => $content,
358             'length' => $totallen
359             };
360              
361 1         6 $i += (2 + $fld_count * 2);
362              
363 1         12 push @tmpl, { $template_id => $content };
364             } ## end while ($i < $count)
365              
366 1         11 return @tmpl;
367             } ## end sub _parse_template_v9
368              
369             sub _debug {
370 1     1   5 my ($self, @msg) = @_;
371 1         5 (undef, undef, my $line) = caller;
372 1         449 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 2     2   12 use strict;
  2         4  
  2         41  
398 2     2   14 use warnings;
  2         3  
  2         73  
399              
400 2         10 use fields qw/
401             flowsets
402             header
403             templates
404             parsed
405             unparsed
406 2     2   10 /;
  2         2  
407              
408             {
409 2     2   151 no strict 'refs';
  2         3  
  2         670  
410             foreach my $k (keys %{'Netflow::Parser::Packet::FIELDS'}) {
411 1     1   14947 *{$k} = sub { shift->{$k} }
412             }
413             };
414              
415             sub new {
416 1     1   4 my Netflow::Parser::Packet $self = shift;
417 1         7 my (%opts) = @_;
418 1 50       7 unless (ref $self) {
419 1         18 $self = fields::new($self);
420             }
421              
422             $self->{header}
423 1         134 = Netflow::Parser::Packet::Header->new(%{ delete $opts{header} });
  1         19  
424              
425 1         6 $self->{flowsets} = delete $opts{flowsets};
426              
427 1         6 foreach (qw/templates parsed unparsed/) {
428 3         16 $self->{$_} = [];
429             }
430              
431 1         6 return $self;
432             } ## end sub new
433              
434             sub add_template {
435 0     0   0 my ($self, $tmpl) = @_;
436 0         0 push @{ $self->{templates} }, $tmpl;
  0         0  
437             }
438              
439             sub add_parsed {
440 1     1   4 my ($self, $hr) = @_;
441 1         2 push @{ $self->{parsed} }, $hr;
  1         10  
442             }
443              
444             sub add_unparsed {
445 0     0   0 my ($self, $hr) = @_;
446 0         0 push @{ $self->{unparsed} }, $hr;
  0         0  
447             }
448              
449             }
450              
451             1; # End of Netflow::Parser::Packet
452              
453             {
454              
455             package Netflow::Parser::Packet::Header;
456 2     2   16 use strict;
  2         4  
  2         61  
457 2     2   10 use warnings;
  2         4  
  2         76  
458              
459 2         38 use fields qw/
460             version
461             count
462             sysuptime
463             unix_secs
464             seqno
465             source_id
466 2     2   9 /;
  2         4  
467              
468             my @fields;
469             {
470 2     2   194 no strict 'refs';
  2         4  
  2         406  
471             @fields = keys %{'Netflow::Parser::Packet::Header::FIELDS'};
472             foreach (@fields) {
473 0     0   0 *{$_} = sub { shift->{$_} }
474             }
475             };
476              
477             sub new {
478 1     1   4 my Netflow::Parser::Packet::Header $self = shift;
479 1         6 my (%opts) = @_;
480 1 50       6 unless (ref $self) {
481 1         6 $self = fields::new($self);
482             }
483              
484 1         126 foreach (@fields) {
485 6 50       27 $opts{$_} || next;
486 6         29 $self->{$_} = delete $opts{$_};
487             }
488              
489 1         6 return $self;
490             } ## end sub new
491             }
492              
493             1; # End of Netflow::Parser::Packet::Header