File Coverage

blib/lib/App/RecordStream/Operation/fromtcpdump.pm
Criterion Covered Total %
statement 33 96 34.3
branch 0 18 0.0
condition n/a
subroutine 11 16 68.7
pod n/a
total 44 130 33.8


line stmt bran cond sub pod time code
1             package App::RecordStream::Operation::fromtcpdump;
2              
3             our $VERSION = "4.0.23";
4              
5 1     1   423 use strict;
  1         2  
  1         25  
6 1     1   4 use warnings;
  1         2  
  1         22  
7              
8 1     1   5 use base qw(App::RecordStream::Operation);
  1         2  
  1         64  
9              
10 1     1   6 use App::RecordStream::OptionalRequire qw(NetPacket::Ethernet :ALL);
  1         2  
  1         7  
11 1     1   7 use App::RecordStream::OptionalRequire qw(NetPacket::IP :ALL);
  1         3  
  1         3  
12 1     1   4 use App::RecordStream::OptionalRequire qw(NetPacket::TCP :ALL);
  1         3  
  1         3  
13 1     1   5 use App::RecordStream::OptionalRequire qw(NetPacket::UDP :ALL);
  1         3  
  1         4  
14 1     1   5 use App::RecordStream::OptionalRequire qw(NetPacket::ARP :ALL);
  1         2  
  1         4  
15 1     1   5 use App::RecordStream::OptionalRequire qw(Net::Pcap pcap_open_offline pcap_loop pcap_next_ex);
  1         2  
  1         4  
16 1     1   4 use App::RecordStream::OptionalRequire qw(Net::DNS::Packet);
  1         2  
  1         4  
17             App::RecordStream::OptionalRequire::require_done();
18              
19 1     1   10 use Data::Dumper;
  1         3  
  1         923  
20              
21             # From NetPacket::IP
22             my $IP_FLAGS = {
23             'more_fragments' => IP_FLAG_MOREFRAGS,
24             'dont_fragment' => IP_FLAG_DONTFRAG,
25             'congestion' => IP_FLAG_CONGESTION,
26             };
27              
28             # From NetPacket::TCP
29             my $TCP_FLAGS = {
30             FIN => FIN,
31             SYN => SYN,
32             RST => RST,
33             PSH => PSH,
34             ACK => ACK,
35             URG => URG,
36             ECE => ECE,
37             CWR => CWR,
38             };
39              
40             # From NetPacket::ARP_OPCODES
41             my $ARP_OPCODES = {
42             +ARP_OPCODE_REQUEST , 'ARP_REQUEST',
43             +ARP_OPCODE_REPLY , 'ARP_REPLY',
44             +RARP_OPCODE_REQUEST , 'RARP_REQUEST',
45             +RARP_OPCODE_REPLY , 'RARP_REPLY',
46             };
47              
48             my $DEFAULT_SUPPRESSED_FIELDS = [qw(data _frame _parent type)];
49              
50             sub init {
51 0     0     my $this = shift;
52 0           my $args = shift;
53              
54 0           my $data = 0;
55 0           my $spec = {
56             'data' => \$data,
57             };
58              
59 0           $this->parse_options($args, $spec);
60              
61 0 0         if ( ! @$args ) {
62 0           die "Missing capture file\n";
63             }
64              
65 0           $this->{'FILES'} = $args;
66 0           $this->{'DATA'} = $data;
67             }
68              
69             sub wants_input {
70 0     0     return 0;
71             }
72              
73             sub stream_done {
74 0     0     my $this = shift;
75              
76 0           foreach my $filename ( @{$this->{'FILES'}} ) {
  0            
77 0           $this->update_current_filename($filename);
78             # TODO: have a connections output rather than packets
79 0           $this->dump_packets($filename);
80             }
81             }
82              
83             sub dump_packets {
84 0     0     my $this = shift;
85 0           my $file = shift;
86              
87 0           my $error;
88 0           my $pcap = pcap_open_offline($file, \$error);
89              
90 0 0         die $error if ( $error );
91              
92 0           my ($raw_packet, %header);
93 0           while(pcap_next_ex($pcap, \%header, \$raw_packet) == 1) {
94              
95             my $record = {
96             'length' => $header{'len'},
97 0           'caplen' => $header{'caplen'},
98             'file' => $file,
99             };
100              
101 0 0         if ( $header{'tv_sec'} ) {
102 0           $record->{'timestamp'} = join('.', $header{'tv_sec'}, $header{'tv_usec'});
103             }
104              
105 0           $this->push_record($this->create_packet_record($raw_packet, $record));
106             }
107             }
108              
109             # Packet parsing courtesy of Net::Analysis
110             sub create_packet_record {
111 0     0     my $this = shift;
112 0           my $packet = shift;
113 0           my $record = shift;
114              
115 0           my ($eth_obj) = NetPacket::Ethernet->decode($packet);
116              
117 0           $this->propagate_fields('ethernet', $eth_obj, $record);
118 0           my $type = 'ethernet';
119 0           my $data = $eth_obj->{'data'};
120              
121 0 0         if ($eth_obj->{type} == ETH_TYPE_IP) {
    0          
122 0           my $ip_obj = NetPacket::IP->decode($eth_obj->{data});
123 0           $this->propagate_fields('ip', $ip_obj, $record, [qw(flags)]);
124 0           $type = 'ip';
125 0           $data = $ip_obj->{'data'};
126              
127             $record->{'ip'}->{'flags'} = $this->get_flag_list(
128 0           $ip_obj->{'flags'},
129             $IP_FLAGS,
130             );
131              
132 0 0         if($ip_obj->{proto} == IP_PROTO_TCP) {
    0          
133             # Some ethernet frames come with padding; this confuses NetPacket,
134             # so strip it off here before parsing the IP payload as a TCP
135             # packet.
136 0           my $ip_data_len = $ip_obj->{len} - $ip_obj->{hlen} * 4;
137 0 0         if ($ip_data_len < length($ip_obj->{data})) {
138 0           my $truncated_data = substr($ip_obj->{'data'}, 0, $ip_data_len);
139 0           $ip_obj->{'data'} = $truncated_data;
140             }
141              
142 0           my $tcp_obj = NetPacket::TCP->decode($ip_obj->{data});
143 0           $this->propagate_fields('tcp', $tcp_obj, $record);
144 0           $type = 'tcp';
145 0           $data = $tcp_obj->{'data'};
146              
147             $record->{'tcp'}->{'flags'} = $this->get_flag_list(
148 0           $tcp_obj->{'flags'},
149             $TCP_FLAGS,
150             );
151              
152 0           $this->attach_dns_info($record, $tcp_obj);
153             }
154             elsif ( $ip_obj->{'proto'} == IP_PROTO_UDP ) {
155 0           my $udp_obj = NetPacket::UDP->decode ($ip_obj->{data});
156 0           $this->propagate_fields('udp', $udp_obj, $record);
157 0           $type = 'udp';
158 0           $data = $udp_obj->{'data'};
159              
160 0           $this->attach_dns_info($record, $udp_obj);
161             }
162             }
163             elsif ( $eth_obj->{'type'} == ETH_TYPE_ARP ) {
164 0           $type = 'arp';
165 0           my $arp_obj = NetPacket::ARP->decode($eth_obj->{data});
166 0           $this->propagate_fields('arp', $arp_obj, $record, [qw(opcode)]);
167              
168 0           my $opcode = $arp_obj->{'opcode'};
169 0           $record->{'arp'}->{'opcode'} = $ARP_OPCODES->{$opcode};
170             }
171              
172 0           $record->{'type'} = $type;
173 0 0         $record->{'data'} = $data if ( $this->{'DATA'} );
174              
175 0           return App::RecordStream::Record->new($record);
176             }
177              
178             sub attach_dns_info {
179             my $this = shift;
180             my $record = shift;
181             my $packet = shift;
182              
183             # Assume DNS packets happen on port 53
184             unless ( $packet->{'dest_port'} == 53 || $packet->{'src_port'} == 53 ) {
185             return;
186             }
187              
188             my $data = $packet->{'data'};
189             my $dns_packet = Net::DNS::Packet->new(\$data);
190             my @answers = $dns_packet->answer();
191              
192             if ( ! $this->{'DATA'} ) {
193             $dns_packet->{'buffer'} = '';
194             foreach my $answer (@answers) {
195             $answer->{'rdata'} = '';
196             }
197             }
198              
199             $record->{'dns'} = $dns_packet;
200             $record->{'dns'}->{'answer'} = \@answers;
201             }
202              
203             sub get_flag_list {
204             my $this = shift;
205             my $flags = shift;
206             my $flags_hash = shift;
207              
208             my $to_return = {};
209             foreach my $name ( keys %$flags_hash ) {
210             if ( $flags & $flags_hash->{$name} ) {
211             $to_return->{$name} = 1;
212             }
213             }
214              
215             return $to_return;
216             }
217              
218              
219             sub propagate_fields {
220             my $this = shift;
221             my $dest_key = shift;
222             my $src = shift;
223             my $dest = shift;
224             my $extra_suppressed = shift;
225              
226             my $suppressed = { map { $_ => 1 } @$DEFAULT_SUPPRESSED_FIELDS, @$extra_suppressed };
227              
228             foreach my $key (keys %$src) {
229             next if ( $suppressed->{$key} );
230             $dest->{$dest_key}->{$key} = $src->{$key};
231             }
232             }
233              
234             sub usage {
235             my $this = shift;
236              
237             my $options = [
238             [ 'data', 'Include raw data bytes of deepest packet level'],
239             ];
240              
241             my $args_string = $this->options_string($options);
242              
243             my $ip_flag_names = join(', ', sort keys %$IP_FLAGS);
244             my $tcp_flag_names = join(', ', sort keys %$TCP_FLAGS);
245             my $arp_opcodes = join(', ', sort values %$ARP_OPCODES);
246              
247             return <
248             Usage: recs-fromtcpdump ...
249             __FORMAT_TEXT__
250             Runs tcpdump and puts out records, one for each packet. Expects pcap
251             files. Will put the name of the originating capture file in the 'file'
252             field.
253              
254             Will parse packet types: ethernet, ip, udp, arp, tcp
255             The type key will indicate the highest level parsed. DNS information will
256             be parsed for TCP or UDP packets that are from or to port 53. The parsed
257             representation of the packet for each valid level will be placed in the
258             corresponding key. For instance, for a tcp packet, there will be
259             information in the keys 'ethernet', 'ip', and 'tcp'
260              
261             By default, data output is suppressed due to poor interaction with terminal
262             programs.
263             __FORMAT_TEXT__
264              
265             Flags will be parsed into hash of strings
266             Possible IP flags: $ip_flag_names
267             Poassible TCP flags: $tcp_flag_names
268              
269             ARP opcodes will be matched
270             Possible opcodes: $arp_opcodes
271              
272             Creating a pcap file:
273             __FORMAT_TEXT__
274             Run a tcpdump command with -w FILE to produce a pcap file. For instance:
275             sudo tcpdump -w /var/tmp/capture.pcap
276              
277             Optionally, include all the data and timing information:
278             sudo tcpdump -w capture.pcap -s4096 -S -tt
279              
280             See 'man tcpdump' for more information.
281             __FORMAT_TEXT__
282              
283             Arguments
284             $args_string
285              
286             Examples
287             Get records for all packets
288             recs-fromtcpdump capture.pcap
289             USAGE
290             }
291              
292             1;