File Coverage

blib/lib/Netflow/Parser.pm
Criterion Covered Total %
statement 15 112 13.3
branch 0 46 0.0
condition 0 6 0.0
subroutine 5 13 38.4
pod 4 4 100.0
total 24 181 13.2


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