File Coverage

blib/lib/IPTables/Parse.pm
Criterion Covered Total %
statement 17 423 4.0
branch 0 264 0.0
condition 0 230 0.0
subroutine 6 20 30.0
pod 5 12 41.6
total 28 949 2.9


line stmt bran cond sub pod time code
1             #
2             ##################################################################
3             #
4             # File: IPTables::Parse.pm
5             #
6             # Purpose: Perl interface to parse iptables and ip6tables rulesets.
7             #
8             # Author: Michael Rash (mbr@cipherdyne.org)
9             #
10             # Version: 1.5
11             #
12             ##################################################################
13             #
14              
15             package IPTables::Parse;
16              
17 1     1   11188 use 5.006;
  1         3  
18 1     1   807 use POSIX ":sys_wait_h";
  1         7795  
  1         6  
19 1     1   1364 use Carp;
  1         20  
  1         59  
20 1     1   5 use strict;
  1         1  
  1         29  
21 1     1   5 use warnings;
  1         1  
  1         27  
22 1     1   4 use vars qw($VERSION);
  1         1  
  1         5908  
23              
24             $VERSION = '1.5';
25              
26             sub new() {
27 0     0 0   my $class = shift;
28 0           my %args = @_;
29              
30 0           my $ipt_bin = '/sbin/iptables';
31 0           my $ipt6_bin = '/sbin/ip6tables';
32 0           my $fwc_bin = '/usr/bin/firewall-cmd';
33              
34             my $self = {
35             _iptables => $args{'iptables'} || $args{'ip6tables'} || '',
36             _firewall_cmd => $args{'firewall-cmd'} || '',
37             _fwd_args => $args{'fwd_args'} || '--direct --passthrough ipv4',
38             _ipv6 => $args{'use_ipv6'} || 0,
39             _iptout => $args{'iptout'} || '/tmp/ipt.out' . $$,
40             _ipterr => $args{'ipterr'} || '/tmp/ipt.err' . $$,
41             _ipt_alarm => $args{'ipt_alarm'} || 30,
42             _debug => $args{'debug'} || 0,
43             _verbose => $args{'verbose'} || 0,
44             _ipt_rules_file => $args{'ipt_rules_file'} || '',
45             _ipt_exec_style => $args{'ipt_exec_style'} || 'waitpid',
46             _ipt_exec_sleep => $args{'ipt_exec_sleep'} || 0,
47             _sigchld_handler => $args{'sigchld_handler'} || \&REAPER,
48 0   0       _skip_ipt_exec_check => $args{'skip_ipt_exec_check'} || 0
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
49             };
50              
51 0 0         if ($self->{'_skip_ipt_exec_check'}) {
52 0 0 0       unless ($self->{'_firewall_cmd'} or $self->{'_iptables'}) {
53             ### default
54 0           $self->{'_iptables'} = $ipt_bin;
55             }
56             } else {
57 0 0         if ($self->{'_firewall_cmd'}) {
    0          
58             croak "[*] $self->{'_firewall_cmd'} incorrect path.\n"
59 0 0         unless -e $self->{'_firewall_cmd'};
60             croak "[*] $self->{'_firewall_cmd'} not executable.\n"
61 0 0         unless -x $self->{'_firewall_cmd'};
62             } elsif ($self->{'_iptables'}) {
63             croak "[*] $self->{'_iptables'} incorrect path.\n"
64 0 0         unless -e $self->{'_iptables'};
65             croak "[*] $self->{'_iptables'} not executable.\n"
66 0 0         unless -x $self->{'_iptables'};
67             } else {
68             ### check for firewall-cmd first since systems with it
69             ### will have iptables installed as well (but firewall-cmd
70             ### should be used instead if it exists)
71 0 0 0       if (-e $fwc_bin and -x $fwc_bin) {
    0 0        
    0 0        
72 0           $self->{'_firewall_cmd'} = $fwc_bin;
73             } elsif (-e $ipt_bin and -x $ipt_bin) {
74 0           $self->{'_iptables'} = $ipt_bin;
75             } elsif (-e $ipt6_bin and -x $ipt6_bin) {
76 0           $self->{'_iptables'} = $ipt6_bin;
77             } else {
78 0           croak "[*] Could not find/execute iptables, " .
79             "specify path via _iptables\n";
80             }
81             }
82             }
83              
84 0 0 0       if ($self->{'_ipv6'} and $self->{'_iptables'} eq $ipt_bin) {
85 0 0 0       if (-e $ipt6_bin and -x $ipt6_bin) {
86 0           $self->{'_iptables'} = $ipt6_bin;
87             } else {
88 0           croak "[*] Could not find/execute ip6tables, " .
89             "specify path via _iptables\n";
90             }
91             }
92              
93             ### set the firewall binary name
94 0           $self->{'_ipt_bin_name'} = 'iptables';
95 0 0         if ($self->{'_firewall_cmd'}) {
96 0 0         $self->{'_ipt_bin_name'} = $1 if $self->{'_firewall_cmd'} =~ m|.*/(\S+)|;
97             } else {
98 0 0         $self->{'_ipt_bin_name'} = $1 if $self->{'_iptables'} =~ m|.*/(\S+)|;
99             }
100              
101             ### handle ipv6
102 0 0         if ($self->{'_ipv6'}) {
103 0 0         if ($self->{'_firewall_cmd'}) {
104 0 0         if ($self->{'_fwd_args'} =~ /ipv4/i) {
105 0           $self->{'_fwd_args'} = '--direct --passthrough ipv6';
106             }
107             } else {
108 0 0         if ($self->{'_ipt_bin_name'} eq 'iptables') {
109 0 0         unless ($self->{'_skip_ipt_exec_check'}) {
110 0           croak "[*] use_ipv6 is true, " .
111             "but $self->{'_iptables'} not ip6tables.\n";
112             }
113             }
114             }
115             }
116              
117 0 0         $self->{'_ipv6'} = 1 if $self->{'_ipt_bin_name'} eq 'ip6tables';
118 0 0         if ($self->{'_firewall_cmd'}) {
119 0 0         $self->{'_ipv6'} = 1 if $self->{'_fwd_args'} =~ /ipv6/;
120             }
121              
122             ### set the main command string to allow for iptables execution
123             ### via firewall-cmd if necessary
124 0           $self->{'_cmd'} = $self->{'_iptables'};
125 0 0         if ($self->{'_firewall_cmd'}) {
126 0           $self->{'_cmd'} = "$self->{'_firewall_cmd'} $self->{'_fwd_args'}";
127             }
128              
129 0           $self->{'parse_keys'} = &parse_keys();
130              
131 0           bless $self, $class;
132             }
133              
134             sub DESTROY {
135 0     0     my $self = shift;
136              
137             ### clean up tmp files
138 0 0         unless ($self->{'_debug'}) {
139 0           unlink $self->{'_iptout'};
140 0           unlink $self->{'_ipterr'};
141             }
142              
143 0           return;
144             }
145              
146             sub parse_keys() {
147 0     0 0   my $self = shift;
148              
149             ### only used for IPv4 + NAT
150 0           my $ipv4_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
151              
152 0           my %keys = (
153             'regular' => {
154             'packets' => {
155             'regex' => '',
156             'ipt_match' => ''
157             },
158             'bytes' => {
159             'regex' => '',
160             'ipt_match' => ''
161             },
162             'target' => {
163             'regex' => '',
164             'ipt_match' => ''
165             },
166             'protocol' => {
167             'regex' => '',
168             'ipt_match' => '-p'
169             },
170             'proto' => {
171             'regex' => '',
172             'ipt_match' => '-p'
173             },
174             'intf_in' => {
175             'regex' => '',
176             'ipt_match' => '-i'
177             },
178             'intf_out' => {
179             'regex' => '',
180             'ipt_match' => '-o'
181             },
182             'src' => {
183             'regex' => '',
184             'ipt_match' => '-s'
185             },
186             'dst' => {
187             'regex' => '',
188             'ipt_match' => '-d'
189             }
190             },
191             'extended' => {
192             's_port' => {
193             'regex' => qr/\bspts?:(\S+)/,
194             'ipt_match' => '--sport'
195             },
196             'sport' => {
197             'regex' => qr/\bspts?:(\S+)/,
198             'ipt_match' => '--sport'
199             },
200             'd_port' => {
201             'regex' => qr/\bdpts?:(\S+)/,
202             'ipt_match' => '--dport'
203             },
204             'dport' => {
205             'regex' => qr/\bdpts?:(\S+)/,
206             'ipt_match' => '--dport'
207             },
208             'to_ip' => {
209             'regex' => qr/\bto:($ipv4_re):\d+/,
210             'ipt_match' => ''
211             },
212             'to_port' => {
213             'regex' => qr/\bto:$ipv4_re:(\d+)/,
214             'ipt_match' => ''
215             },
216             'mac_source' => {
217             'regex' => qr/\bMAC\s+(\S+)/,
218             'ipt_match' => '-m mac --mac-source'
219             },
220             'state' => {
221             'regex' => qr/\bstate\s+(\S+)/,
222             'ipt_match' => '-m state --state'
223             },
224             'ctstate' => {
225             'regex' => qr/\bctstate\s+(\S+)/,
226             'ipt_match' => '-m conntrack --ctstate'
227             },
228             'comment' => {
229             'regex' => qr|\/\*\s(.*?)\s\*\/|,
230             'ipt_match' => '-m comment --comment',
231             'use_quotes' => 1
232             },
233             'string' => {
234             'regex' => qr|STRING\s+match\s+\"(.*?)\"|,
235             'ipt_match' => '-m string --algo bm --string',
236             'use_quotes' => 1
237             },
238             'length' => {
239             'regex' => qr|\blength\s(\S+)|,
240             'ipt_match' => '-m length --length',
241             },
242             },
243             'rule_num' => '',
244             'raw' => ''
245             );
246              
247 0           return \%keys;
248             }
249              
250             sub list_table_chains() {
251 0     0 1   my $self = shift;
252 0   0       my $table = shift || croak '[*] Specify a table, e.g. "nat"';
253 0   0       my $file = shift || '';
254              
255 0           my @ipt_lines = ();
256 0           my @chains = ();
257              
258 0 0 0       if ($self->{'_ipt_rules_file'} and not $file) {
259 0           $file = $self->{'_ipt_rules_file'};
260             }
261              
262 0 0         if ($file) {
263             ### read the iptables rules out of $file instead of executing
264             ### the iptables command.
265 0 0         open F, "< $file" or croak "[*] Could not open file $file: $!";
266 0           @ipt_lines = ;
267 0           close F;
268             } else {
269 0           my ($rv, $out_ar, $err_ar) = $self->exec_iptables(
270             "$self->{'_cmd'} -t $table -v -n -L");
271 0           @ipt_lines = @$out_ar;
272             }
273              
274 0           for (@ipt_lines) {
275 0 0         if (/^\s*Chain\s(.*?)\s\(/) {
276 0           push @chains, $1;
277             }
278             }
279 0           return \@chains;
280             }
281              
282             sub chain_policy() {
283 0     0 1   my $self = shift;
284 0   0       my $table = shift || croak '[*] Specify a table, e.g. "nat"';
285 0   0       my $chain = shift || croak '[*] Specify a chain, e.g. "OUTPUT"';
286 0   0       my $file = shift || '';
287              
288 0           my @ipt_lines = ();
289              
290 0 0 0       if ($self->{'_ipt_rules_file'} and not $file) {
291 0           $file = $self->{'_ipt_rules_file'};
292             }
293              
294 0 0         if ($file) {
295             ### read the iptables rules out of $file instead of executing
296             ### the iptables command.
297 0 0         open F, "< $file" or croak "[*] Could not open file $file: $!";
298 0           @ipt_lines = ;
299 0           close F;
300             } else {
301 0           my ($rv, $out_ar, $err_ar) = $self->exec_iptables(
302             "$self->{'_cmd'} -t $table -v -n -L $chain");
303 0           @ipt_lines = @$out_ar;
304             }
305              
306 0           my $policy = '';
307              
308 0           for my $line (@ipt_lines) {
309             ### Chain INPUT (policy ACCEPT 16 packets, 800 bytes)
310 0 0         if ($line =~ /^\s*Chain\s+$chain\s+\(policy\s+(\w+)/) {
311 0           $policy = $1;
312 0           last;
313             }
314             }
315              
316 0           return $policy;
317             }
318              
319             sub chain_action_rules() {
320 0     0 0   return &chain_rules();
321             }
322              
323             sub chain_rules() {
324 0     0 1   my $self = shift;
325 0   0       my $table = shift || croak '[*] Specify a table, e.g. "nat"';
326 0   0       my $chain = shift || croak '[*] Specify a chain, e.g. "OUTPUT"';
327 0   0       my $file = shift || '';
328              
329 0           my $found_chain = 0;
330 0           my @ipt_lines = ();
331              
332             ### only used for IPv4 + NAT
333 0           my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;
334              
335             ### array of hash refs
336 0           my @chain = ();
337 0           my @global_accept_state = ();
338              
339 0 0 0       if ($self->{'_ipt_rules_file'} and not $file) {
340 0           $file = $self->{'_ipt_rules_file'};
341             }
342              
343 0 0         if ($file) {
344             ### read the iptables rules out of $file instead of executing
345             ### the iptables command.
346 0 0         open F, "< $file" or croak "[*] Could not open file $file: $!";
347 0           @ipt_lines = ;
348 0           close F;
349             } else {
350 0           my ($rv, $out_ar, $err_ar) = $self->exec_iptables(
351             "$self->{'_cmd'} -t $table -v -n -L $chain --line-numbers");
352 0           @ipt_lines = @$out_ar;
353             }
354              
355             ### determine the output style (e.g. "-nL -v" or just plain "-nL"; if the
356             ### policy data came from a file then -v might not have been used)
357 0           my $ipt_verbose = 0;
358 0           for my $line (@ipt_lines) {
359 0 0         if ($line =~ /\spkts\s+bytes\s+target/) {
360 0           $ipt_verbose = 1;
361 0           last;
362             }
363             }
364 0           my $has_line_numbers = 0;
365 0           for my $line (@ipt_lines) {
366 0 0         if ($line =~ /^num\s+pkts\s+bytes\s+target/) {
367 0           $has_line_numbers = 1;
368 0           last;
369             }
370             }
371              
372 0           my $rule_num = 0;
373              
374 0           LINE: for my $line (@ipt_lines) {
375 0           chomp $line;
376              
377 0 0 0       last LINE if ($found_chain and $line =~ /^\s*Chain\s+/);
378              
379 0 0         if ($line =~ /^\s*Chain\s\Q$chain\E\s\(/i) {
380 0           $found_chain = 1;
381 0           next LINE;
382             }
383 0 0         next LINE if $line =~ /\starget\s{2,}prot/i;
384 0 0         next LINE unless $found_chain;
385 0 0         next LINE unless $line;
386              
387             ### track the rule number independently of --line-numbers,
388             ### but the values should always match
389 0           $rule_num++;
390              
391             ### initialize hash
392 0           my %rule = (
393             'extended' => '',
394             'raw' => $line,
395             'rule_num' => $rule_num
396             );
397 0           for my $key (keys %{$self->{'parse_keys'}->{'regular'}}) {
  0            
398 0           $rule{$key} = '';
399             }
400 0           for my $key (keys %{$self->{'parse_keys'}->{'extended'}}) {
  0            
401 0           $rule{$key} = '';
402             }
403              
404 0           my $rule_body = '';
405 0           my $packets = '';
406 0           my $bytes = '';
407 0           my $rnum = '';
408              
409 0 0         if ($ipt_verbose) {
410 0 0         if ($has_line_numbers) {
411 0 0         if ($line =~ /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(.*)/) {
412 0           $rnum = $1;
413 0           $packets = $2;
414 0           $bytes = $3;
415 0           $rule_body = $4;
416             }
417             } else {
418 0 0         if ($line =~ /^\s*(\d+)\s+(\d+)\s+(.*)/) {
419 0           $packets = $1;
420 0           $bytes = $2;
421 0           $rule_body = $3;
422             }
423             }
424             } else {
425 0 0         if ($has_line_numbers) {
426 0 0         if ($line =~ /^\s*(\d+)\s+(\d+)\s+(.*)/) {
427 0           $rnum = $1;
428 0           $rule_body = $2;
429             }
430             }
431             }
432              
433 0 0 0       if ($rnum and $rnum ne $rule_num) {
434 0           croak "[*] Rule number mis-match.";
435             }
436              
437 0 0         if ($ipt_verbose) {
438              
439             ### iptables:
440             ### 0 0 ACCEPT tcp -- eth1 * 192.168.10.3 0.0.0.0/0 tcp dpt:80
441             ### 0 0 ACCEPT tcp -- eth1 * 192.168.10.15 0.0.0.0/0 tcp dpt:22
442             ### 33 2348 ACCEPT tcp -- eth1 * 192.168.10.2 0.0.0.0/0 tcp dpt:22
443             ### 0 0 ACCEPT tcp -- eth1 * 192.168.10.2 0.0.0.0/0 tcp dpt:80
444             ### 0 0 DNAT tcp -- * * 123.123.123.123 0.0.0.0/0 tcp dpt:55000 to:192.168.12.12:80
445              
446             ### ip6tables:
447             ### 0 0 ACCEPT tcp * * ::/0 fe80::aa:0:1/128 tcp dpt:12345
448             ### 0 0 LOG all * * ::/0 ::/0 LOG flags 0 level 4
449              
450 0           my $match_re = qr/^(\S+)\s+(\S+)\s+\-\-\s+
451             (\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*)/x;
452              
453 0 0 0       if ($self->{'_ipt_bin_name'} eq 'ip6tables'
      0        
454             or ($self->{'_ipt_bin_name'} eq 'firewall-cmd'
455             and $self->{'_fwd_args'} =~ /\sipv6/)) {
456 0           $match_re = qr/^(\S+)\s+(\S+)\s+
457             (\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*)/x;
458             }
459              
460 0 0         if ($rule_body =~ $match_re) {
461 0           $rule{'packets'} = $packets;
462 0           $rule{'bytes'} = $bytes;
463 0           $rule{'target'} = $1;
464 0           my $proto = $2;
465 0 0         $proto = 'all' if $proto eq '0';
466 0           $rule{'protocol'} = $rule{'proto'} = lc($proto);
467 0           $rule{'intf_in'} = $3;
468 0           $rule{'intf_out'} = $4;
469 0           $rule{'src'} = $5;
470 0           $rule{'dst'} = $6;
471 0   0       $rule{'extended'} = $7 || '';
472              
473 0           &parse_rule_extended(\%rule, $self->{'parse_keys'}->{'extended'});
474             }
475             } else {
476              
477             ### iptables:
478             ### ACCEPT tcp -- 164.109.8.0/24 0.0.0.0/0 tcp dpt:22 flags:0x16/0x02
479             ### ACCEPT tcp -- 216.109.125.67 0.0.0.0/0 tcp dpts:7000:7500
480             ### ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpts:7000:7500
481             ### ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:!7000
482             ### ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
483             ### ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp spt:35000 dpt:5000
484             ### ACCEPT tcp -- 10.1.1.1 0.0.0.0/0
485              
486             ### LOG all -- 0.0.0.0/0 0.0.0.0/0 LOG flags 0 level 4 prefix `DROP '
487             ### LOG all -- 127.0.0.2 0.0.0.0/0 LOG flags 0 level 4
488             ### DNAT tcp -- 123.123.123.123 0.0.0.0/0 tcp dpt:55000 to:192.168.12.12:80
489              
490             ### ip6tables:
491             ### ACCEPT tcp ::/0 fe80::aa:0:1/128 tcp dpt:12345
492             ### LOG all ::/0 ::/0 LOG flags 0 level 4
493              
494 0           my $match_re = qr/^(\S+)\s+(\S+)\s+\-\-\s+(\S+)\s+(\S+)\s*(.*)/;
495              
496 0 0 0       if ($self->{'_ipt_bin_name'} eq 'ip6tables'
      0        
497             or ($self->{'_ipt_bin_name'} eq 'firewall-cmd'
498             and $self->{'_fwd_args'} =~ /\sipv6/)) {
499 0           $match_re = qr/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*)/;
500             }
501              
502 0 0         if ($rule_body =~ $match_re) {
503 0           $rule{'target'} = $1;
504 0           my $proto = $2;
505 0 0         $proto = 'all' if $proto eq '0';
506 0           $rule{'protocol'} = $rule{'proto'} = lc($proto);
507 0           $rule{'src'} = $3;
508 0           $rule{'dst'} = $4;
509 0   0       $rule{'extended'} = $5 || '';
510              
511 0           &parse_rule_extended(\%rule, $self->{'parse_keys'}->{'extended'});
512             }
513             }
514 0           push @chain, \%rule;
515             }
516 0           return \@chain;
517             }
518              
519             sub parse_rule_extended() {
520 0     0 0   my ($rule_hr, $ext_keys_hr) = @_;
521              
522 0           for my $key (keys %$ext_keys_hr) {
523 0 0         if ($rule_hr->{'extended'}
524             =~ /$ext_keys_hr->{$key}->{'regex'}/) {
525 0           $rule_hr->{$key} = $1;
526             }
527             }
528              
529 0 0 0       if ($rule_hr->{'protocol'} eq '0') {
    0          
530 0           $rule_hr->{'s_port'} = $rule_hr->{'sport'} = 0;
531 0           $rule_hr->{'d_port'} = $rule_hr->{'dport'} = 0;
532             } elsif ($rule_hr->{'protocol'} eq 'tcp'
533             or $rule_hr->{'protocol'} eq 'udp') {
534             $rule_hr->{'s_port'} = $rule_hr->{'sport'} = 0
535 0 0         if $rule_hr->{'s_port'} eq '';
536             $rule_hr->{'d_port'} = $rule_hr->{'dport'} = 0
537 0 0         if $rule_hr->{'d_port'} eq '';
538             }
539              
540 0           return;
541             }
542              
543             sub default_drop() {
544 0     0 1   my $self = shift;
545 0   0       my $table = shift || croak "[*] Specify a table, e.g. \"nat\"";
546 0   0       my $chain = shift || croak "[*] Specify a chain, e.g. \"OUTPUT\"";
547 0   0       my $file = shift || '';
548              
549 0           my @ipt_lines = ();
550              
551 0 0 0       if ($self->{'_ipt_rules_file'} and not $file) {
552 0           $file = $self->{'_ipt_rules_file'};
553             }
554              
555 0 0         if ($file) {
556             ### read the iptables rules out of $file instead of executing
557             ### the iptables command.
558 0 0         open F, "< $file" or croak "[*] Could not open file $file: $!";
559 0           @ipt_lines = ;
560 0           close F;
561             } else {
562             ### FIXME -v for interfaces?
563 0           my ($rv, $out_ar, $err_ar) = $self->exec_iptables(
564             "$self->{'_cmd'} -t $table -n -L $chain");
565 0           @ipt_lines = @$out_ar;
566             }
567              
568 0 0         return "[-] Could not get $self->{'_ipt_bin_name'} output!", 0
569             unless @ipt_lines;
570              
571 0           my %protocols = ();
572 0           my $found_chain = 0;
573 0           my $found_default_drop = 0;
574 0           my $rule_ctr = 1;
575 0           my $prefix;
576 0           my $policy = 'ACCEPT';
577 0           my $any_ip_re = qr/(?:0\.){3}0\x2f0|\x3a{2}\x2f0/;
578              
579 0           LINE: for my $line (@ipt_lines) {
580 0           chomp $line;
581              
582 0 0 0       last if ($found_chain and $line =~ /^\s*Chain\s+/);
583              
584             ### Chain INPUT (policy DROP)
585             ### Chain FORWARD (policy ACCEPT)
586 0 0         if ($line =~ /^\s*Chain\s+$chain\s+\(policy\s+(\w+)\)/) {
587 0           $policy = $1;
588 0           $found_chain = 1;
589             }
590 0 0         next LINE if $line =~ /^\s*target\s/i;
591 0 0         next LINE unless $found_chain;
592              
593             ### include ULOG target as well
594 0           my $log_re = qr/^\s*U?LOG\s+(\w+)\s+\-\-\s+.*
595             $any_ip_re\s+$any_ip_re\s+(.*)/x;
596 0           my $drop_re = qr/^DROP\s+(\w+)\s+\-\-\s+.*
597             $any_ip_re\s+$any_ip_re\s*$/x;
598              
599 0 0 0       if ($self->{'_ipt_bin_name'} eq 'ip6tables'
      0        
600             or ($self->{'_ipt_bin_name'} eq 'firewall-cmd'
601             and $self->{'_fwd_args'} =~ /ipv6/)) {
602 0           $log_re = qr/^\s*U?LOG\s+(\w+)\s+
603             $any_ip_re\s+$any_ip_re\s+(.*)/x;
604 0           $drop_re = qr/^DROP\s+(\w+)\s+
605             $any_ip_re\s+$any_ip_re\s*$/x;
606             }
607              
608             ### might as well pick up any default logging rules as well
609 0 0 0       if ($line =~ $log_re) {
    0          
610 0           my $proto = $1;
611 0           my $p_tmp = $2;
612 0           my $prefix = 'NONE';
613              
614             ### some recent iptables versions return "0" instead of "all"
615             ### for the protocol number
616 0 0         $proto = 'all' if $proto eq '0';
617             ### LOG flags 0 level 4 prefix `DROP '
618 0 0 0       if ($p_tmp && $p_tmp =~ m|LOG.*\s+prefix\s+
619             \`\s*(.+?)\s*\'|x) {
620 0           $prefix = $1;
621             }
622             ### $proto may equal "all" here
623 0           $protocols{$proto}{'LOG'}{'prefix'} = $prefix;
624 0           $protocols{$proto}{'LOG'}{'rulenum'} = $rule_ctr;
625             } elsif ($policy eq 'ACCEPT' and $line =~ $drop_re) {
626 0           my $proto = $1;
627 0 0         $proto = 'all' if $proto eq '0';
628             ### DROP all -- 0.0.0.0/0 0.0.0.0/0
629 0           $protocols{$1}{'DROP'} = $rule_ctr;
630 0           $found_default_drop = 1;
631             }
632 0           $rule_ctr++;
633             }
634              
635             ### if the policy in the chain is DROP, then we don't
636             ### necessarily need to find a default DROP rule.
637 0 0         if ($policy eq 'DROP') {
638 0           $protocols{'all'}{'DROP'} = 0;
639 0           $found_default_drop = 1;
640             }
641              
642 0 0 0       return "[-] There are no default drop rules in the " .
643             "$self->{'_ipt_bin_name'} policy!", 0
644             unless %protocols and $found_default_drop;
645              
646 0           return \%protocols, 1;
647             }
648              
649             sub default_log() {
650 0     0 1   my $self = shift;
651 0   0       my $table = shift || croak "[*] Specify a table, e.g. \"nat\"";
652 0   0       my $chain = shift || croak "[*] Specify a chain, e.g. \"OUTPUT\"";
653 0   0       my $file = shift || '';
654              
655 0           my $any_ip_re = qr/(?:0\.){3}0\x2f0|\x3a{2}\x2f0/;
656 0           my @ipt_lines = ();
657 0           my %log_chains = ();
658 0           my %log_rules = ();
659              
660 0 0 0       if ($self->{'_ipt_rules_file'} and not $file) {
661 0           $file = $self->{'_ipt_rules_file'};
662             }
663              
664             ### note that we are not restricting the view to the current chain
665             ### with the iptables -nL output; we are going to parse the given
666             ### chain and all chains to which packets are jumped from the given
667             ### chain.
668 0 0         if ($file) {
669             ### read the iptables rules out of $file instead of executing
670             ### the iptables command.
671 0 0         open F, "< $file" or croak "[*] Could not open file $file: $!";
672 0           @ipt_lines = ;
673 0           close F;
674             } else {
675 0           my ($rv, $out_ar, $err_ar) = $self->exec_iptables(
676             "$self->{'_cmd'} -t $table -n -L $chain");
677 0           @ipt_lines = @$out_ar;
678             }
679              
680             ### determine the output style (e.g. "-nL -v" or just plain "-nL"; if the
681             ### policy data came from a file then -v might not have been used)
682 0           my $ipt_verbose = 0;
683 0           for my $line (@ipt_lines) {
684 0 0         if ($line =~ /^\s*pkts\s+bytes\s+target/) {
685 0           $ipt_verbose = 1;
686 0           last;
687             }
688             }
689              
690 0 0         return "[-] Could not get $self->{'_ipt_bin_name'} output!", 0
691             unless @ipt_lines;
692              
693             ### first get all logging rules and associated chains
694 0           my $log_chain;
695              
696 0           for my $line (@ipt_lines) {
697 0           chomp $line;
698              
699             ### Chain INPUT (policy DROP)
700             ### Chain fwsnort_INPUT_eth1 (1 references)
701 0 0 0       if ($line =~ /^\s*Chain\s+(.*?)\s+\(/ and
702             $line !~ /0\s+references/) {
703 0           $log_chain = $1;
704             }
705 0 0         $log_chain = '' unless $line =~ /\S/;
706 0 0         next unless $log_chain;
707              
708 0           my $proto = '';
709 0           my $found = 0;
710 0 0         if ($ipt_verbose) {
711 0 0 0       if ($self->{'_ipt_bin_name'} eq 'ip6tables'
      0        
712             or ($self->{'_ipt_bin_name'} eq 'firewall-cmd'
713             and $self->{'_fwd_args'} =~ /\sipv6/)) {
714 0 0         if ($line =~ m|^\s*\d+\s+\d+\s*U?LOG\s+(\w+)\s+
715             \S+\s+\S+\s+$any_ip_re
716             \s+$any_ip_re\s+.*U?LOG|x) {
717 0           $proto = $1;
718 0           $found = 1;
719             }
720             } else {
721 0 0         if ($line =~ m|^\s*\d+\s+\d+\s*U?LOG\s+(\w+)\s+\-\-\s+
722             \S+\s+\S+\s+$any_ip_re
723             \s+$any_ip_re\s+.*U?LOG|x) {
724 0           $proto = $1;
725 0           $found = 1;
726             }
727             }
728             } else {
729 0 0 0       if ($self->{'_ipt_bin_name'} eq 'ip6tables'
      0        
730             or ($self->{'_ipt_bin_name'} eq 'firewall-cmd'
731             and $self->{'_fwd_args'} =~ /\sipv6/)) {
732 0 0         if ($line =~ m|^\s*U?LOG\s+(\w+)\s+$any_ip_re
733             \s+$any_ip_re\s+.*U?LOG|x) {
734 0           $proto = $1;
735 0           $found = 1;
736             }
737             } else {
738 0 0         if ($line =~ m|^\s*U?LOG\s+(\w+)\s+\-\-\s+$any_ip_re
739             \s+$any_ip_re\s+.*U?LOG|x) {
740 0           $proto = $1;
741 0           $found = 1;
742             }
743             }
744             }
745              
746 0 0         if ($found) {
747 0 0         $proto = 'all' if $proto eq '0';
748             ### the above regex allows the limit target to be used
749 0           $log_chains{$log_chain}{$proto} = ''; ### protocol
750 0 0         $log_rules{$proto} = '' if $log_chain eq $chain;
751             }
752             }
753              
754 0 0         return "[-] There are no default logging rules " .
755             "in the $self->{'_ipt_bin_name'} policy!", 0 unless %log_chains;
756              
757 0           my %sub_chains = ();
758              
759             ### get all sub-chains of the main chain we passed into default_log()
760 0           &sub_chains($chain, \%sub_chains, \@ipt_lines);
761              
762             ### see which (if any) logging rules can be mapped back to the
763             ### main chain we passed in.
764 0           for my $log_chain (keys %log_chains) {
765 0 0         if (defined $sub_chains{$log_chain}) {
766             ### the logging rule is in the main chain (e.g. INPUT)
767 0           for my $proto (keys %{$log_chains{$log_chain}}) {
  0            
768 0           $log_rules{$proto} = '';
769             }
770             }
771             }
772              
773 0           return \%log_rules, 1;
774             }
775              
776             sub sub_chains() {
777 0     0 0   my ($start_chain, $chains_hr, $ipt_lines_ar) = @_;
778 0           my $found = 0;
779 0           for my $line (@$ipt_lines_ar) {
780 0           chomp $line;
781             ### Chain INPUT (policy DROP)
782             ### Chain fwsnort_INPUT_eth1 (1 references)
783 0 0 0       if ($line =~ /^\s*Chain\s+\Q$start_chain\E\s+\(/ and
784             $line !~ /0\s+references/) {
785 0           $found = 1;
786 0           next;
787             }
788 0 0         next unless $found;
789 0 0 0       if ($found and $line =~ /^\s*Chain\s/) {
790 0           last;
791             }
792 0 0         if ($line =~ m|^\s*(\S+)\s+\S+\s+|) {
793 0           my $new_chain = $1;
794 0 0 0       if ($new_chain ne 'LOG'
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
795             and $new_chain ne 'DROP'
796             and $new_chain ne 'REJECT'
797             and $new_chain ne 'ACCEPT'
798             and $new_chain ne 'RETURN'
799             and $new_chain ne 'QUEUE'
800             and $new_chain ne 'SNAT'
801             and $new_chain ne 'DNAT'
802             and $new_chain ne 'MASQUERADE'
803             and $new_chain ne 'pkts'
804             and $new_chain ne 'Chain'
805             and $new_chain ne 'target') {
806 0           $chains_hr->{$new_chain} = '';
807 0           &sub_chains($new_chain, $chains_hr, $ipt_lines_ar);
808             }
809             }
810             }
811 0           return;
812             }
813              
814             sub exec_iptables() {
815 0     0 0   my $self = shift;
816 0   0       my $cmd = shift || croak "[*] Must specify an " .
817             "$self->{'_ipt_bin_name'} command to run.";
818 0           my $iptout = $self->{'_iptout'};
819 0           my $ipterr = $self->{'_ipterr'};
820 0           my $debug = $self->{'_debug'};
821 0           my $ipt_alarm = $self->{'_ipt_alarm'};
822 0           my $verbose = $self->{'_verbose'};
823 0           my $ipt_exec_style = $self->{'_ipt_exec_style'};
824 0           my $ipt_exec_sleep = $self->{'_ipt_exec_sleep'};
825 0           my $sigchld_handler = $self->{'_sigchld_handler'};
826              
827 0 0 0       croak "[*] $cmd does not look like an $self->{'_ipt_bin_name'} command."
      0        
      0        
      0        
      0        
828             unless $cmd =~ m|^\s*iptables| or $cmd =~ m|^\S+/iptables|
829             or $cmd =~ m|^\s*ip6tables| or $cmd =~ m|^\S+/ip6tables|
830             or $cmd =~ m|^\s*firewall-cmd| or $cmd =~ m|^\S+/firewall-cmd|;
831              
832             ### sanitize $cmd - this is not bullet proof, but better than
833             ### nothing (especially for strange iptables chain names). Further,
834             ### quotemeta() is too aggressive since things like IPv6 addresses
835             ### contain ":" chars, etc.
836 0           $cmd =~ s/([;<>\$\|`\@&\(\)\[\]\{\}])/\\$1/g;
837              
838 0           my $rv = 1;
839 0           my @stdout = ();
840 0           my @stderr = ();
841              
842 0           my $fh = *STDERR;
843 0 0         $fh = *STDOUT if $verbose;
844              
845 0 0 0       if ($debug or $verbose) {
846 0           print $fh localtime() . " [+] IPTables::Parse::",
847             "exec_iptables(${ipt_exec_style}()) $cmd\n";
848 0 0         if ($ipt_exec_sleep > 0) {
849 0           print $fh localtime() . " [+] IPTables::Parse::",
850             "exec_iptables() sleep seconds: $ipt_exec_sleep\n";
851             }
852             }
853              
854 0 0         if ($ipt_exec_sleep > 0) {
855 0 0 0       if ($debug or $verbose) {
856 0           print $fh localtime() . " [+] IPTables::Parse: ",
857             "sleeping for $ipt_exec_sleep seconds before ",
858             "executing $self->{'_ipt_bin_name'} command.\n";
859             }
860 0           sleep $ipt_exec_sleep;
861             }
862              
863 0 0         if ($ipt_exec_style eq 'system') {
    0          
864 0           system qq{$cmd > $iptout 2> $ipterr};
865             } elsif ($ipt_exec_style eq 'popen') {
866 0 0         open CMD, "$cmd 2> $ipterr |" or croak "[*] Could not execute $cmd: $!";
867 0           @stdout = ;
868 0           close CMD;
869 0 0         open F, "> $iptout" or croak "[*] Could not open $iptout: $!";
870 0           print F for @stdout;
871 0           close F;
872             } else {
873 0           my $ipt_pid;
874              
875 0 0 0       if ($debug or $verbose) {
876 0           print $fh localtime() . " [+] IPTables::Parse: " .
877             "Setting SIGCHLD handler to: " . $sigchld_handler . "\n";
878             }
879              
880 0           local $SIG{'CHLD'} = $sigchld_handler;
881 0 0         if ($ipt_pid = fork()) {
882 0           eval {
883             ### iptables should never take longer than 30 seconds to execute,
884             ### unless there is some absolutely enormous policy or the kernel
885             ### is exceedingly busy
886 0     0     local $SIG{'ALRM'} = sub {die "[*] $self->{'_ipt_bin_name'} " .
887 0           "command timeout.\n"};
888 0           alarm $ipt_alarm;
889 0           waitpid($ipt_pid, 0);
890 0           alarm 0;
891             };
892 0 0         if ($@) {
893 0 0         kill 9, $ipt_pid unless kill 15, $ipt_pid;
894             }
895             } else {
896 0 0         croak "[*] Could not fork $self->{'_ipt_bin_name'}: $!"
897             unless defined $ipt_pid;
898              
899             ### exec the iptables command and preserve stdout and stderr
900 0           exec qq{$cmd > $iptout 2> $ipterr};
901             }
902             }
903              
904 0 0         if (-e $iptout) {
905 0 0         open F, "< $iptout" or croak "[*] Could not open $iptout";
906 0           @stdout = ;
907 0           close F;
908             }
909 0 0         if (-e $ipterr) {
910 0 0         open F, "< $ipterr" or croak "[*] Could not open $ipterr";
911 0           @stderr = ;
912 0           close F;
913              
914 0 0         $rv = 0 if @stderr;
915             }
916              
917 0 0         if (@stdout) {
918 0 0         if ($stdout[$#stdout] =~ /^success/) {
919 0           pop @stdout;
920             }
921 0 0         if ($self->{'_ipt_bin_name'} eq 'firewall-cmd') {
922 0           for (@stdout) {
923 0 0         if (/COMMAND_FAILED/) {
924 0           $rv = 0;
925 0           last;
926             }
927             }
928             }
929             }
930              
931 0 0 0       if ($debug or $verbose) {
932 0           print $fh localtime() . " $self->{'_ipt_bin_name'} " .
933             "command stdout:\n";
934 0           for my $line (@stdout) {
935 0 0         if ($line =~ /\n$/) {
936 0           print $fh $line;
937             } else {
938 0           print $fh $line, "\n";
939             }
940             }
941 0           print $fh localtime() . " $self->{'_ipt_bin_name'} " .
942             "command stderr:\n";
943 0           for my $line (@stderr) {
944 0 0         if ($line =~ /\n$/) {
945 0           print $fh $line;
946             } else {
947 0           print $fh $line, "\n";
948             }
949             }
950             }
951              
952 0 0 0       if ($debug or $verbose) {
953 0           print $fh localtime() . " Return value: $rv\n";
954             }
955              
956 0           return $rv, \@stdout, \@stderr;
957             }
958              
959             sub REAPER {
960 0     0 0   my $stiff;
961 0           while(($stiff = waitpid(-1,WNOHANG))>0){
962             # do something with $stiff if you want
963             }
964 0           local $SIG{'CHLD'} = \&REAPER;
965 0           return;
966             }
967              
968             1;
969             __END__