File Coverage

blib/lib/IPTables/Parse.pm
Criterion Covered Total %
statement 20 439 4.5
branch 0 276 0.0
condition 0 232 0.0
subroutine 7 21 33.3
pod 5 12 41.6
total 32 980 3.2


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